From 93ec9641eec65498360a3114775756a4efbd93bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:05:35 +0900 Subject: [PATCH 01/49] spring-gift-product -> spring-gift-wishlist --- .../gift/controller/ProductController.java | 69 ++++++++ .../java/gift/controller/ProductRequest.java | 41 +++++ src/main/java/gift/domain/Product.java | 56 ++++++ .../gift/repository/ProductDBRepository.java | 79 +++++++++ .../repository/ProductMemoryRepository.java | 58 +++++++ .../gift/repository/ProductRepository.java | 16 ++ .../java/gift/service/ProductService.java | 59 +++++++ src/main/resources/schema.sql | 6 + .../resources/templates/product-add-form.html | 28 +++ .../templates/product-edit-form.html | 29 ++++ .../resources/templates/product-list.html | 34 ++++ .../controller/ProductControllerTest.java | 129 ++++++++++++++ .../repository/ProductDBRepositoryTest.java | 159 +++++++++++++++++ .../ProductMemoryRepositoryTest.java | 156 +++++++++++++++++ .../java/gift/service/ProductServiceTest.java | 163 ++++++++++++++++++ 15 files changed, 1082 insertions(+) create mode 100644 src/main/java/gift/controller/ProductController.java create mode 100644 src/main/java/gift/controller/ProductRequest.java create mode 100644 src/main/java/gift/domain/Product.java create mode 100644 src/main/java/gift/repository/ProductDBRepository.java create mode 100644 src/main/java/gift/repository/ProductMemoryRepository.java create mode 100644 src/main/java/gift/repository/ProductRepository.java create mode 100644 src/main/java/gift/service/ProductService.java create mode 100644 src/main/resources/schema.sql create mode 100644 src/main/resources/templates/product-add-form.html create mode 100644 src/main/resources/templates/product-edit-form.html create mode 100644 src/main/resources/templates/product-list.html create mode 100644 src/test/java/gift/controller/ProductControllerTest.java create mode 100644 src/test/java/gift/repository/ProductDBRepositoryTest.java create mode 100644 src/test/java/gift/repository/ProductMemoryRepositoryTest.java create mode 100644 src/test/java/gift/service/ProductServiceTest.java diff --git a/src/main/java/gift/controller/ProductController.java b/src/main/java/gift/controller/ProductController.java new file mode 100644 index 000000000..b12128bfb --- /dev/null +++ b/src/main/java/gift/controller/ProductController.java @@ -0,0 +1,69 @@ +package gift.controller; + +import gift.domain.Product; +import gift.service.ProductService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.util.*; + +@Controller +//@RequestMapping("/api/products") +public class ProductController { + + private final ProductService productService; + + @Autowired + public ProductController(ProductService productService) + { + this.productService = productService; + } + + @GetMapping("/api/products") + public String getProducts(Model model){ + model.addAttribute("products", productService.findProducts()); + return "product-list"; + } + + @GetMapping("/api/products/{id}") + public String getProduct(@PathVariable Long id, Model model){ + model.addAttribute("products", productService.findOne(id)); + return "product-list"; + } + + @GetMapping("/api/products/new") + public String newProductForm(Model model){ + model.addAttribute("product", new ProductRequest()); + return "product-add-form"; + } + + @PostMapping("/api/products") + public String addProduct(@ModelAttribute ProductRequest productRequest) { + productService.register(productRequest); + return "redirect:/api/products"; + } + + @GetMapping("/api/products/edit/{id}") + public String editProductForm(@PathVariable long id, Model model){ + Optional product = productService.findOne(id); + if (product.isPresent()){ + model.addAttribute("product", product.get()); + return "product-edit-form"; + }; + return "redirect:/api/products"; + } + @PostMapping("/api/products/edit/{id}") + public String updateProduct(@PathVariable Long id, @ModelAttribute ProductRequest productRequest) { + productService.update(id, productRequest); + return "redirect:/api/products"; + } + + @GetMapping("/api/products/delete/{id}") + public String deleteProduct(@PathVariable Long id){ + productService.delete(id); + return "redirect:/api/products"; + + } +} diff --git a/src/main/java/gift/controller/ProductRequest.java b/src/main/java/gift/controller/ProductRequest.java new file mode 100644 index 000000000..5fa089661 --- /dev/null +++ b/src/main/java/gift/controller/ProductRequest.java @@ -0,0 +1,41 @@ +package gift.controller; + +import gift.domain.Product; + +public class ProductRequest { + private String name; + private long price; + private String imageUrl; + + public ProductRequest(String name, long price, String imageUrl) { + this.name = name; + this.price = price; + this.imageUrl = imageUrl; + } + + public ProductRequest() { + } + + public String getName(){ + return name; + } + public void setName(String name){ + this.name = name; + } + public long getPrice(){ + return price; + } + public void setPrice(long price){ + this.price = price; + } + public String getImageUrl(){ + return imageUrl; + } + public void setImageUrl(String imageUrl){ + this.imageUrl = imageUrl; + } + + public static ProductRequest entityToRequest(Product product){ + return new ProductRequest(product.getName(), product.getPrice(), product.getImageUrl()); + } +} diff --git a/src/main/java/gift/domain/Product.java b/src/main/java/gift/domain/Product.java new file mode 100644 index 000000000..a38ea489d --- /dev/null +++ b/src/main/java/gift/domain/Product.java @@ -0,0 +1,56 @@ +package gift.domain; + + +import gift.controller.ProductRequest; + +public class Product { + private long id; + private String name; + private long price; + private String imageUrl; + + public Product(){} + + public Product(long id, String name, long price, String imageUrl) { + this.id = id; + this.name = name; + this.price = price; + this.imageUrl = imageUrl; + } + + + public Product(String name, long price, String imageUrl) { + 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 long getPrice(){ + return price; + } + public void setPrice(long price){ + this.price = price; + } + public String getImageUrl(){ + return imageUrl; + } + public void setImageUrl(String imageUrl){ + this.imageUrl = imageUrl; + } + + public static Product RequestToEntity(ProductRequest productRequest){ + return new Product(productRequest.getName(), productRequest.getPrice(), productRequest.getImageUrl()); + } +} diff --git a/src/main/java/gift/repository/ProductDBRepository.java b/src/main/java/gift/repository/ProductDBRepository.java new file mode 100644 index 000000000..b5c2d5acd --- /dev/null +++ b/src/main/java/gift/repository/ProductDBRepository.java @@ -0,0 +1,79 @@ +package gift.repository; + +import gift.controller.ProductRequest; +import gift.domain.Product; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.stereotype.Repository; + +import javax.sql.DataSource; +import java.util.*; + +@Repository +public class ProductDBRepository implements ProductRepository { + + private final JdbcTemplate jdbcTemplate; + + public ProductDBRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public Product save(Product product) { + GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); + String sql = "INSERT INTO product(name, price, imageUrl) VALUES (?, ?, ?)"; + jdbcTemplate.update(sql, product.getName(), product.getPrice(), product.getImageUrl(), keyHolder); + product.setId(keyHolder.getKey().longValue()); + return product; + } + + @Override + public Optional findById(Long id) { + String sql = "SELECT * FROM product WHERE id = ?"; + List products = jdbcTemplate.query(sql, productRowMapper(), id); + return products.stream().findAny(); + } + + @Override + public Optional findByName(String name) { + String sql = "SELECT * FROM product WHERE name = ?"; + List products = jdbcTemplate.query(sql, productRowMapper(), name); + return products.stream().findAny(); + } + + @Override + public List findAll() { + return jdbcTemplate.query("select * from product", productRowMapper()); + } + + @Override + public Optional updateById(Long id, ProductRequest productRequest){ + String sql = "UPDATE product " + + "SET name = ?,price=?,imageUrl=? " + + "WHERE id = ?"; + jdbcTemplate.update(sql,productRequest.getName(), productRequest.getPrice(), productRequest.getImageUrl(), id); + return findById(id); + } + + @Override + public Optional deleteById(Long id) { + Optional deleted_product = findById(id); + String sql = "DELETE FROM product WHERE id = ?"; + jdbcTemplate.update(sql, id); + return deleted_product; + } + + private RowMapper productRowMapper() { + return (rs, rowNum) -> { + Product product = new Product( + rs.getLong("id"), + rs.getString("name"), + rs.getLong("price"), + rs.getString("imageUrl") + ); + return product; + }; + } +} diff --git a/src/main/java/gift/repository/ProductMemoryRepository.java b/src/main/java/gift/repository/ProductMemoryRepository.java new file mode 100644 index 000000000..c814fe823 --- /dev/null +++ b/src/main/java/gift/repository/ProductMemoryRepository.java @@ -0,0 +1,58 @@ +package gift.repository; + +import gift.controller.ProductRequest; +import gift.domain.Product; +import org.springframework.stereotype.Repository; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public class ProductMemoryRepository implements ProductRepository { + private static final Map products = new ConcurrentHashMap<>(); + private static AtomicLong idCounter = new AtomicLong(); + //final 추가해서 Map에 다른 객체 할당되는거 방지 + //static 추가해서 여러 레포지토리가 있을때도 같은 변수 공유함 + + @Override + public Product save(Product product){ + product.setId(idCounter.incrementAndGet()); + products.put(product.getId(), product); + return product; + } + + @Override + public Optional findById(Long id) { + return Optional.ofNullable(products.get(id)); + } + + @Override + public Optional findByName(String name){ + return products.values().stream().filter(product -> product.getName().equals(name)).findAny(); + } + + @Override + public List findAll() { + return new ArrayList<>(products.values()); + } + + @Override + public Optional updateById(Long id, ProductRequest productRequest){ + Product product = products.get(id); + product.setName(productRequest.getName()); + product.setPrice(productRequest.getPrice()); + product.setImageUrl(productRequest.getImageUrl()); + return Optional.of(product); + } + + @Override + public Optional deleteById(Long id){ + Optional product = findById(id); + products.remove(id); + return product; + } + + + + +} diff --git a/src/main/java/gift/repository/ProductRepository.java b/src/main/java/gift/repository/ProductRepository.java new file mode 100644 index 000000000..5c6a26f3a --- /dev/null +++ b/src/main/java/gift/repository/ProductRepository.java @@ -0,0 +1,16 @@ +package gift.repository; + +import gift.controller.ProductRequest; +import gift.domain.Product; + +import java.util.List; +import java.util.Optional; + +public interface ProductRepository { + Product save(Product product); + Optional findById(Long id); + Optional findByName(String name); + List findAll(); + Optional updateById(Long id, ProductRequest productRequest); + Optional deleteById(Long id); +} diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java new file mode 100644 index 000000000..b86d0760a --- /dev/null +++ b/src/main/java/gift/service/ProductService.java @@ -0,0 +1,59 @@ +package gift.service; + +import gift.controller.ProductRequest; +import gift.domain.Product; +import gift.repository.ProductRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +@Service +@Transactional() +public class ProductService { + private final ProductRepository productRepository; + + @Autowired + public ProductService(ProductRepository productRepository){ + this.productRepository = productRepository; + } + + public Product register(ProductRequest productRequest){ + validateDuplicateProduct(productRequest); + Product product = Product.RequestToEntity(productRequest); + return productRepository.save(product); + } + private void validateDuplicateProduct(ProductRequest productRequest){ + productRepository.findByName(productRequest.getName()) + .ifPresent(p -> { + throw new IllegalStateException("이미 존재하는 상품입니다."); + }); + } + + public List findProducts(){ + return productRepository.findAll(); + } + + public Optional findOne(Long productId){ + return productRepository.findById(productId); + } + + public Product update(Long productId, ProductRequest productRequest){ + Optional product = productRepository.updateById(productId, productRequest); + if (product.isPresent()){ + return product.get(); + }; + throw new NoSuchElementException("존재하지 않는 상품입니다."); + + } + + public Optional delete(Long productId){ + return productRepository.deleteById(productId); + } +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..cca12d009 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE product ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + price BIGINT NOT NULL, + imageUrl VARCHAR(255) +); \ No newline at end of file diff --git a/src/main/resources/templates/product-add-form.html b/src/main/resources/templates/product-add-form.html new file mode 100644 index 000000000..1479a42fd --- /dev/null +++ b/src/main/resources/templates/product-add-form.html @@ -0,0 +1,28 @@ + + + + + Product Form + + +

Product Form

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+Back to Product List + + diff --git a/src/main/resources/templates/product-edit-form.html b/src/main/resources/templates/product-edit-form.html new file mode 100644 index 000000000..91110caa6 --- /dev/null +++ b/src/main/resources/templates/product-edit-form.html @@ -0,0 +1,29 @@ + + + + + Product Form + + +

Product Form

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+Back to Product List + + diff --git a/src/main/resources/templates/product-list.html b/src/main/resources/templates/product-list.html new file mode 100644 index 000000000..40db63784 --- /dev/null +++ b/src/main/resources/templates/product-list.html @@ -0,0 +1,34 @@ + + + + + Product List + + +

Product List

+Add New Product + + + + + + + + + + + + + + + + + + + +
IDNamePriceImage URLActions
product image + Edit + Delete +
+ + diff --git a/src/test/java/gift/controller/ProductControllerTest.java b/src/test/java/gift/controller/ProductControllerTest.java new file mode 100644 index 000000000..975138ed9 --- /dev/null +++ b/src/test/java/gift/controller/ProductControllerTest.java @@ -0,0 +1,129 @@ +package gift.controller; + +import gift.controller.ProductController; +import gift.controller.ProductRequest; +import gift.domain.Product; +import gift.service.ProductService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.Arrays; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@WebMvcTest(ProductController.class) +@AutoConfigureMockMvc +public class ProductControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private ProductService productService; + + @BeforeEach + void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + void getProducts() throws Exception { + Product product1 = new Product(1L, "Product 1", 100L, "url-1"); + Product product2 = new Product(2L, "Product 2", 200L, "url-2"); + + when(productService.findProducts()) + .thenReturn(Arrays.asList(product1, product2)); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/products")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.model().attributeExists("products")) + .andExpect(MockMvcResultMatchers.view().name("product-list")); + } + + @Test + void getProduct() throws Exception { + Long productId = 1L; + Product product = new Product(productId, "Test Product", 150L, "test-url"); + + when(productService.findOne(productId)) + .thenReturn(Optional.of(product)); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/products/{id}", productId)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.model().attributeExists("products")) + .andExpect(MockMvcResultMatchers.view().name("product-list")); + } + + @Test + void newProductForm() throws Exception { + mockMvc.perform(MockMvcRequestBuilders.get("/api/products/new")) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.model().attributeExists("product")) + .andExpect(MockMvcResultMatchers.view().name("product-add-form")); + } + + @Test + void addProduct() throws Exception { + ProductRequest productRequest = new ProductRequest("New Product", 100L, "new-product-url"); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/products") + .param("name", productRequest.getName()) + .param("price", String.valueOf(productRequest.getPrice())) + .param("imageUrl", productRequest.getImageUrl())) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) + .andExpect(MockMvcResultMatchers.view().name("redirect:/api/products")); + + verify(productService, times(1)).register(any(ProductRequest.class)); + } + + @Test + void editProductForm() throws Exception { + Long productId = 1L; + Product product = new Product(productId, "Editable Product", 200L, "edit-url"); + + when(productService.findOne(productId)) + .thenReturn(Optional.of(product)); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/products/edit/{id}", productId)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.model().attributeExists("product")) + .andExpect(MockMvcResultMatchers.view().name("product-edit-form")); + } + + @Test + void updateProduct() throws Exception { + Long productId = 1L; + ProductRequest updatedProductRequest = new ProductRequest("Updated Product", 300L, "updated-url"); + + mockMvc.perform(MockMvcRequestBuilders.post("/api/products/edit/{id}", productId) + .param("name", updatedProductRequest.getName()) + .param("price", String.valueOf(updatedProductRequest.getPrice())) + .param("imageUrl", updatedProductRequest.getImageUrl())) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) + .andExpect(MockMvcResultMatchers.view().name("redirect:/api/products")); + + verify(productService, times(1)).update(eq(productId), any(ProductRequest.class)); + } + + @Test + void deleteProduct() throws Exception { + Long productId = 1L; + + mockMvc.perform(MockMvcRequestBuilders.get("/api/products/delete/{id}", productId)) + .andExpect(MockMvcResultMatchers.status().is3xxRedirection()) + .andExpect(MockMvcResultMatchers.view().name("redirect:/api/products")); + + verify(productService, times(1)).delete(productId); + } +} diff --git a/src/test/java/gift/repository/ProductDBRepositoryTest.java b/src/test/java/gift/repository/ProductDBRepositoryTest.java new file mode 100644 index 000000000..96e39b340 --- /dev/null +++ b/src/test/java/gift/repository/ProductDBRepositoryTest.java @@ -0,0 +1,159 @@ +package gift.repository; + +import gift.controller.ProductRequest; +import gift.domain.Product; +import gift.repository.ProductDBRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; + +import javax.sql.DataSource; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.when; + +public class ProductDBRepositoryTest { + + private ProductDBRepository repository; + + @Mock + private DataSource dataSource; + + @Mock + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + MockitoAnnotations.initMocks(this); + repository = new ProductDBRepository(dataSource); + } + + @Test + void saveProduct() { + Product product = new Product("Test Product", 100, "test-url"); + + when(jdbcTemplate.update(anyString(), any(), any(), any())).thenReturn(1); + + Product savedProduct = repository.save(product); + + assertNotNull(savedProduct); + assertEquals(product.getName(), savedProduct.getName()); + assertEquals(product.getPrice(), savedProduct.getPrice()); + assertEquals(product.getImageUrl(), savedProduct.getImageUrl()); + } + + @Test + void findById_id있을때() { + Product product = new Product(1L, "Test Product", 100, "test-url"); + + when(jdbcTemplate.query(anyString(), any(RowMapper.class), anyLong())) + .thenReturn(Arrays.asList(product)); + + Optional foundProduct = repository.findById(1L); + + assertTrue(foundProduct.isPresent()); + assertEquals(product, foundProduct.get()); + } + + @Test + void findById_id없을때() { + when(jdbcTemplate.query(anyString(), any(RowMapper.class), anyLong())) + .thenReturn(Arrays.asList()); + + Optional foundProduct = repository.findById(999L); + + assertFalse(foundProduct.isPresent()); + } + + @Test + void findByName_id있을때() { + Product product = new Product(1L, "Test Product", 100, "test-url"); + + when(jdbcTemplate.query(anyString(), any(RowMapper.class), anyString())) + .thenReturn(Arrays.asList(product)); + + Optional foundProduct = repository.findByName("Test Product"); + + assertTrue(foundProduct.isPresent()); + assertEquals(product, foundProduct.get()); + } + + @Test + void findByName_id없을때() { + when(jdbcTemplate.query(anyString(), any(RowMapper.class), anyString())) + .thenReturn(Arrays.asList()); + + Optional foundProduct = repository.findByName("Non Existing Product"); + + assertFalse(foundProduct.isPresent()); + } + + @Test + void findAll_returnsAllProducts() { + Product product1 = new Product(1L, "Product 1", 100, "url-1"); + Product product2 = new Product(2L, "Product 2", 200, "url-2"); + + when(jdbcTemplate.query(anyString(), any(RowMapper.class))) + .thenReturn(Arrays.asList(product1, product2)); + + List allProducts = repository.findAll(); + + assertEquals(2, allProducts.size()); + } + + @Test + void updateById_id있을때() { + ProductRequest updatedProductRequest = new ProductRequest("Updated Product", 200, "updated-url"); + + when(jdbcTemplate.update(anyString(), any(), any(), any(), any())) + .thenReturn(1); + + Optional updatedProductOptional = repository.updateById(1L, updatedProductRequest); + + assertTrue(updatedProductOptional.isPresent()); + Product updatedProduct = updatedProductOptional.get(); + assertEquals(updatedProductRequest.getName(), updatedProduct.getName()); + assertEquals(updatedProductRequest.getPrice(), updatedProduct.getPrice()); + assertEquals(updatedProductRequest.getImageUrl(), updatedProduct.getImageUrl()); + } + + @Test + void updateById_id없을때() { + when(jdbcTemplate.update(anyString(), any(), any(), any(), any())) + .thenReturn(0); + + Optional updatedProductOptional = repository.updateById(999L, new ProductRequest("Updated Product", 200, "updated-url")); + + assertFalse(updatedProductOptional.isPresent()); + } + + @Test + void deleteById_id있을때() { + Product product = new Product(1L, "To Be Deleted", 100, "delete-me-url"); + + when(jdbcTemplate.query(anyString(), any(RowMapper.class), anyLong())) + .thenReturn(Arrays.asList(product)); + + Optional deletedProductOptional = repository.deleteById(1L); + + assertTrue(deletedProductOptional.isPresent()); + assertEquals(product, deletedProductOptional.get()); + } + + @Test + void deleteById_id없을때() { + when(jdbcTemplate.query(anyString(), any(RowMapper.class), anyLong())) + .thenReturn(Arrays.asList()); + + Optional deletedProductOptional = repository.deleteById(999L); + + assertFalse(deletedProductOptional.isPresent()); + } +} diff --git a/src/test/java/gift/repository/ProductMemoryRepositoryTest.java b/src/test/java/gift/repository/ProductMemoryRepositoryTest.java new file mode 100644 index 000000000..dfec2ff51 --- /dev/null +++ b/src/test/java/gift/repository/ProductMemoryRepositoryTest.java @@ -0,0 +1,156 @@ +package gift.repository; + +import gift.controller.ProductRequest; +import gift.domain.Product; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.jupiter.api.Assertions.*; + +public class ProductMemoryRepositoryTest { + + private ProductMemoryRepository repository; + + @BeforeEach + void setUp() { + repository = new ProductMemoryRepository(); + } + + @Test + void saveProduct() { + Product product = new Product("Test Product", 100, "test-url"); + + Product savedProduct = repository.save(product); + + assertEquals(product.getName(), savedProduct.getName()); + assertEquals(product.getPrice(), savedProduct.getPrice()); + assertEquals(product.getImageUrl(), savedProduct.getImageUrl()); + assertTrue(savedProduct.getId() > 0); + } + + @Test + void findById_id있을때() { + Product product = new Product("Test Product", 100, "test-url"); + Product savedProduct = repository.save(product); + Long productId = savedProduct.getId(); + + Optional foundProduct = repository.findById(productId); + + assertTrue(foundProduct.isPresent()); + assertEquals(savedProduct, foundProduct.get()); + } + + @Test + void findById_id없을때() { + Optional foundProduct = repository.findById(999L); + + assertFalse(foundProduct.isPresent()); + } + + @Test + void findByName_name있을때() { + Product product = new Product("Test Product", 100, "test-url"); + repository.save(product); + String productName = product.getName(); + + Optional foundProduct = repository.findByName(productName); + + assertTrue(foundProduct.isPresent()); + assertEquals(productName, foundProduct.get().getName()); + } + + @Test + void findByName_name없을때() { + Optional foundProduct = repository.findByName("Non Existing Product"); + + assertFalse(foundProduct.isPresent()); + } + + @Test + void findAll() { + Product product1 = new Product("Product 1", 100, "url-1"); + Product product2 = new Product("Product 2", 200, "url-2"); + repository.save(product1); + repository.save(product2); + + List allProducts = repository.findAll(); + + assertEquals(2, allProducts.size()); + } + + @Test + void updateById_id있을때() { + Product product = new Product("Initial Product", 100, "initial-url"); + Product savedProduct = repository.save(product); + Long productId = savedProduct.getId(); + ProductRequest updatedProductRequest = new ProductRequest("Updated Product", 200, "updated-url"); + + Optional updatedProductOptional = repository.updateById(productId, updatedProductRequest); + + assertTrue(updatedProductOptional.isPresent()); + Product updatedProduct = updatedProductOptional.get(); + assertEquals(updatedProductRequest.getName(), updatedProduct.getName()); + assertEquals(updatedProductRequest.getPrice(), updatedProduct.getPrice()); + assertEquals(updatedProductRequest.getImageUrl(), updatedProduct.getImageUrl()); + } + + @Test + void updateById_id없을때() { + Optional updatedProductOptional = repository.updateById(999L, new ProductRequest("Updated Product", 200, "updated-url")); + + assertFalse(updatedProductOptional.isPresent()); + } + + @Test + void deleteById_id있을때() { + Product product = new Product("To Be Deleted", 100, "delete-me-url"); + Product savedProduct = repository.save(product); + Long productId = savedProduct.getId(); + + Optional deletedProductOptional = repository.deleteById(productId); + + assertTrue(deletedProductOptional.isPresent()); + assertEquals(product, deletedProductOptional.get()); + assertFalse(repository.findById(productId).isPresent()); + } + + @Test + void deleteById_id없을때() { + Optional deletedProductOptional = repository.deleteById(999L); + + assertFalse(deletedProductOptional.isPresent()); + } + + //동시성 테스트 + @Test + public void testConcurrentProductCreation() throws InterruptedException { + int numberOfThreads = 100; + ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); + Set productIds = ConcurrentHashMap.newKeySet(); + CountDownLatch latch = new CountDownLatch(numberOfThreads); + + for (int i = 0; i < numberOfThreads; i++) { + executorService.submit(() -> { + Product product = new Product("Product", 100, "http://example.com/image.png"); + repository.save(product); + productIds.add(product.getId()); + latch.countDown(); + }); + } + + latch.await(); + executorService.shutdown(); + + // Check if there are any duplicate IDs + assertEquals(numberOfThreads, productIds.size(), "Duplicates found! Number of unique IDs: " + productIds.size()); + } + +} \ No newline at end of file diff --git a/src/test/java/gift/service/ProductServiceTest.java b/src/test/java/gift/service/ProductServiceTest.java new file mode 100644 index 000000000..f2be7633d --- /dev/null +++ b/src/test/java/gift/service/ProductServiceTest.java @@ -0,0 +1,163 @@ +package gift.service; + +import gift.controller.ProductRequest; +import gift.domain.Product; +import gift.repository.ProductRepository; +import gift.service.ProductService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +public class ProductServiceTest { + + @Mock + private ProductRepository productRepository; + + @InjectMocks + private ProductService productService; + + @BeforeEach + void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + void register() { + ProductRequest productRequest = new ProductRequest("New Product", 100L, "new-product-url"); + Product product = new Product(1L, productRequest.getName(), productRequest.getPrice(), productRequest.getImageUrl()); + + when(productRepository.findByName(productRequest.getName())) + .thenReturn(Optional.empty()); + when(productRepository.save(any(Product.class))) + .thenReturn(product); + + Product registeredProduct = productService.register(productRequest); + + assertNotNull(registeredProduct); + assertEquals(product.getName(), registeredProduct.getName()); + assertEquals(product.getPrice(), registeredProduct.getPrice()); + assertEquals(product.getImageUrl(), registeredProduct.getImageUrl()); + } + + @Test + void register_중복() { + ProductRequest productRequest = new ProductRequest("Duplicate Product", 200L, "duplicate-product-url"); + Product existingProduct = new Product(1L, productRequest.getName(), productRequest.getPrice(), productRequest.getImageUrl()); + + when(productRepository.findByName(productRequest.getName())) + .thenReturn(Optional.of(existingProduct)); + + IllegalStateException exception = assertThrows(IllegalStateException.class, + () -> productService.register(productRequest)); + + assertEquals("이미 존재하는 상품입니다.", exception.getMessage()); + } + + @Test + void findProducts() { + Product product1 = new Product(1L, "Product 1", 100L, "url-1"); + Product product2 = new Product(2L, "Product 2", 200L, "url-2"); + + when(productRepository.findAll()) + .thenReturn(Arrays.asList(product1, product2)); + + List foundProducts = productService.findProducts(); + + assertEquals(2, foundProducts.size()); + } + + @Test + void findOne_id존재() { + Product product = new Product(1L, "Found Product", 150L, "found-product-url"); + + when(productRepository.findById(1L)) + .thenReturn(Optional.of(product)); + + Optional foundProduct = productService.findOne(1L); + + assertTrue(foundProduct.isPresent()); + assertEquals(product.getName(), foundProduct.get().getName()); + assertEquals(product.getPrice(), foundProduct.get().getPrice()); + assertEquals(product.getImageUrl(), foundProduct.get().getImageUrl()); + } + + @Test + void findOne_id존재x() { + when(productRepository.findById(999L)) + .thenReturn(Optional.empty()); + + Optional foundProduct = productService.findOne(999L); + + assertFalse(foundProduct.isPresent()); + } + + @Test + void update_id존재() { + Long productId = 1L; + ProductRequest updatedProductRequest = new ProductRequest("Updated Product", 300L, "updated-product-url"); + Product updatedProduct = new Product(productId, updatedProductRequest.getName(), updatedProductRequest.getPrice(), updatedProductRequest.getImageUrl()); + + when(productRepository.updateById(productId, updatedProductRequest)) + .thenReturn(Optional.of(updatedProduct)); + + Product result = productService.update(productId, updatedProductRequest); + + assertNotNull(result); + assertEquals(updatedProduct.getName(), result.getName()); + assertEquals(updatedProduct.getPrice(), result.getPrice()); + assertEquals(updatedProduct.getImageUrl(), result.getImageUrl()); + } + + @Test + void update_id존재x() { + Long productId = 999L; + ProductRequest updatedProductRequest = new ProductRequest("Updated Product", 300L, "updated-product-url"); + + when(productRepository.updateById(productId, updatedProductRequest)) + .thenReturn(Optional.empty()); + + NoSuchElementException exception = assertThrows(NoSuchElementException.class, + () -> productService.update(productId, updatedProductRequest)); + + assertEquals("존재하지 않는 상품입니다.", exception.getMessage()); + } + + @Test + void delete_id존재() { + Long productId = 1L; + Product product = new Product(productId, "To Be Deleted", 200L, "delete-me-url"); + + when(productRepository.deleteById(productId)) + .thenReturn(Optional.of(product)); + + Optional result = productService.delete(productId); + + assertTrue(result.isPresent()); + assertEquals(product.getName(), result.get().getName()); + assertEquals(product.getPrice(), result.get().getPrice()); + assertEquals(product.getImageUrl(), result.get().getImageUrl()); + } + + @Test + void delete_id존재x() { + Long productId = 999L; + + when(productRepository.deleteById(productId)) + .thenReturn(Optional.empty()); + + Optional result = productService.delete(productId); + + assertFalse(result.isPresent()); + } +} From 1e6e759d75c7310b4f49d96aa92af85284525a6b Mon Sep 17 00:00:00 2001 From: Jintaek Jeong <87135698+jjt4515@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:07:51 +0900 Subject: [PATCH 02/49] Update README.md --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8376bdfff..4761f39ba 100644 --- a/README.md +++ b/README.md @@ -1 +1,16 @@ -# spring-gift-wishlist \ No newline at end of file +# spring-gift-wishlist + +### 기능 요구 사항 +상품을 추가하거나 수정하는 경우, 클라이언트로부터 잘못된 값이 전달될 수 있다. 잘못된 값이 전달되면 클라이언트가 어떤 부분이 왜 잘못되었는지 인지할 수 있도록 응답을 제공한다. + +- 상품 이름은 공백을 포함하여 최대 15자까지 입력할 수 있다. +- 특수 문자 + - 가능: ( ), [ ], +, -, &, /, _ + - 그 외 특수 문자 사용 불가 +- "카카오"가 포함된 문구는 담당 MD와 협의한 경우에만 사용할 수 있다. + +### 구현 기능 목록 + +- 상품 이름 글자수 제한 +- 특수 문자 제한 +- "카카오"가 포함된 문구 제한 From deb443818c4e1254debae2fda23db1e8718322a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Wed, 3 Jul 2024 23:37:10 +0900 Subject: [PATCH 03/49] =?UTF-8?q?fix:=20ProductDBRepository=EC=9D=98=20sav?= =?UTF-8?q?e=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gift/repository/ProductDBRepository.java | 18 +++++++++++++++--- src/main/java/gift/service/ProductService.java | 1 + src/main/resources/application.properties | 5 +++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/gift/repository/ProductDBRepository.java b/src/main/java/gift/repository/ProductDBRepository.java index b5c2d5acd..9b1b98ab2 100644 --- a/src/main/java/gift/repository/ProductDBRepository.java +++ b/src/main/java/gift/repository/ProductDBRepository.java @@ -6,9 +6,12 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import javax.sql.DataSource; +import java.sql.PreparedStatement; +import java.sql.Statement; import java.util.*; @Repository @@ -22,10 +25,19 @@ public ProductDBRepository(DataSource dataSource) { @Override public Product save(Product product) { - GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); String sql = "INSERT INTO product(name, price, imageUrl) VALUES (?, ?, ?)"; - jdbcTemplate.update(sql, product.getName(), product.getPrice(), product.getImageUrl(), keyHolder); - product.setId(keyHolder.getKey().longValue()); + + KeyHolder keyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + ps.setString(1, product.getName()); + ps.setLong(2, product.getPrice()); + ps.setString(3, product.getImageUrl()); + return ps; + }, keyHolder); + + product.setId(Objects.requireNonNull(keyHolder.getKey()).longValue()); return product; } diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java index b86d0760a..66954daaf 100644 --- a/src/main/java/gift/service/ProductService.java +++ b/src/main/java/gift/service/ProductService.java @@ -5,6 +5,7 @@ import gift.repository.ProductRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3d16b65f4..428453ea7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,6 @@ 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.h2.console.enabled=true \ No newline at end of file From 38b1f06d23af26e99a0bee3f2dc92b891e52d832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Thu, 4 Jul 2024 14:55:15 +0900 Subject: [PATCH 04/49] =?UTF-8?q?chore:=20validation=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ src/main/java/gift/controller/ProductRequest.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index df7db9334..88dba29aa 100644 --- a/build.gradle +++ b/build.gradle @@ -21,9 +21,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + } tasks.named('test') { diff --git a/src/main/java/gift/controller/ProductRequest.java b/src/main/java/gift/controller/ProductRequest.java index 5fa089661..0b5ec9fc2 100644 --- a/src/main/java/gift/controller/ProductRequest.java +++ b/src/main/java/gift/controller/ProductRequest.java @@ -2,7 +2,9 @@ import gift.domain.Product; + public class ProductRequest { + private String name; private long price; private String imageUrl; From de9e5fc8c748fc2b6aad97d4f2708028493390ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Thu, 4 Jul 2024 15:08:43 +0900 Subject: [PATCH 05/49] =?UTF-8?q?feat:=20ProductRequest=20validation=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gift/controller/ProductRequest.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/gift/controller/ProductRequest.java b/src/main/java/gift/controller/ProductRequest.java index 0b5ec9fc2..3a87c8de9 100644 --- a/src/main/java/gift/controller/ProductRequest.java +++ b/src/main/java/gift/controller/ProductRequest.java @@ -1,29 +1,50 @@ package gift.controller; import gift.domain.Product; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; public class ProductRequest { + @NotBlank(message = "상품 이름은 필수 항목입니다.") + @Size(max = 15, message = "상품 이름은 최대 15자까지 입력할 수 있습니다.") + @Pattern( + regexp = "^[a-zA-Z0-9()\\[\\]+\\-&/_ ]*$", + message = "상품 이름에 허용되지 않는 특수 문자가 포함되어 있습니다." + ) + private String name; private long price; private String imageUrl; public ProductRequest(String name, long price, String imageUrl) { + if (name.contains("카카오") && !isApprovedByMD()) { + throw new IllegalArgumentException("상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."); + } this.name = name; this.price = price; this.imageUrl = imageUrl; } - public ProductRequest() { - } + public ProductRequest() {} public String getName(){ return name; } public void setName(String name){ + if (name.contains("카카오") && !isApprovedByMD()) { + throw new IllegalArgumentException("상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."); + } this.name = name; } + + private boolean isApprovedByMD() { + // MD 승인 여부 확인 로직 (임시로 false 리턴) + return false; + } + public long getPrice(){ return price; } From 6679f103a61dc85579fb51a400899da4a9ac0fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:22:55 +0900 Subject: [PATCH 06/49] =?UTF-8?q?feat:=20ProductService=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gift/service/ProductService.java | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java index 66954daaf..e85f79121 100644 --- a/src/main/java/gift/service/ProductService.java +++ b/src/main/java/gift/service/ProductService.java @@ -26,35 +26,29 @@ public ProductService(ProductRepository productRepository){ } public Product register(ProductRequest productRequest){ - validateDuplicateProduct(productRequest); Product product = Product.RequestToEntity(productRequest); - return productRepository.save(product); - } - private void validateDuplicateProduct(ProductRequest productRequest){ - productRepository.findByName(productRequest.getName()) - .ifPresent(p -> { - throw new IllegalStateException("이미 존재하는 상품입니다."); - }); + try { + return productRepository.save(product); + } catch (DataIntegrityViolationException e) { + throw new IllegalArgumentException("상품 데이터가 유효하지 않습니다: " + e.getMessage(), e); + } + } public List findProducts(){ return productRepository.findAll(); } - public Optional findOne(Long productId){ - return productRepository.findById(productId); + public Product findOne(Long productId){ + return productRepository.findById(productId).orElseThrow(() -> new NoSuchElementException("존재하지 않는 상품입니다.")); } public Product update(Long productId, ProductRequest productRequest){ - Optional product = productRepository.updateById(productId, productRequest); - if (product.isPresent()){ - return product.get(); - }; - throw new NoSuchElementException("존재하지 않는 상품입니다."); + return productRepository.updateById(productId, productRequest).orElseThrow(() -> new NoSuchElementException("존재하지 않는 상품입니다.")); } - public Optional delete(Long productId){ - return productRepository.deleteById(productId); + public Product delete(Long productId){ + return productRepository.deleteById(productId).orElseThrow(() -> new NoSuchElementException("존재하지 않는 상품입니다.")); } } From 803f1fa6fff9a961eb820423236a12e0908a2665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:32:56 +0900 Subject: [PATCH 07/49] =?UTF-8?q?feat:=20ProductController=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gift/controller/ProductController.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/gift/controller/ProductController.java b/src/main/java/gift/controller/ProductController.java index b12128bfb..1132d2abe 100644 --- a/src/main/java/gift/controller/ProductController.java +++ b/src/main/java/gift/controller/ProductController.java @@ -2,6 +2,7 @@ import gift.domain.Product; import gift.service.ProductService; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -40,22 +41,19 @@ public String newProductForm(Model model){ } @PostMapping("/api/products") - public String addProduct(@ModelAttribute ProductRequest productRequest) { + public String addProduct(@Valid @ModelAttribute ProductRequest productRequest) { productService.register(productRequest); return "redirect:/api/products"; } @GetMapping("/api/products/edit/{id}") public String editProductForm(@PathVariable long id, Model model){ - Optional product = productService.findOne(id); - if (product.isPresent()){ - model.addAttribute("product", product.get()); - return "product-edit-form"; - }; - return "redirect:/api/products"; + Product product = productService.findOne(id); + model.addAttribute("product", ProductRequest.entityToRequest(product)); + return "product-edit-form"; } @PostMapping("/api/products/edit/{id}") - public String updateProduct(@PathVariable Long id, @ModelAttribute ProductRequest productRequest) { + public String updateProduct(@PathVariable Long id, @Valid @ModelAttribute ProductRequest productRequest) { productService.update(id, productRequest); return "redirect:/api/products"; } From 8bcde301307dd33ea3cf1d8a8b04d94784536cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:17:53 +0900 Subject: [PATCH 08/49] =?UTF-8?q?style:=20ProductDBRepository=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/controller/ProductRequest.java | 2 +- src/main/java/gift/repository/ProductDBRepository.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/gift/controller/ProductRequest.java b/src/main/java/gift/controller/ProductRequest.java index 3a87c8de9..5b2c186d9 100644 --- a/src/main/java/gift/controller/ProductRequest.java +++ b/src/main/java/gift/controller/ProductRequest.java @@ -14,8 +14,8 @@ public class ProductRequest { regexp = "^[a-zA-Z0-9()\\[\\]+\\-&/_ ]*$", message = "상품 이름에 허용되지 않는 특수 문자가 포함되어 있습니다." ) - private String name; + private long price; private String imageUrl; diff --git a/src/main/java/gift/repository/ProductDBRepository.java b/src/main/java/gift/repository/ProductDBRepository.java index 9b1b98ab2..0d9fe9f06 100644 --- a/src/main/java/gift/repository/ProductDBRepository.java +++ b/src/main/java/gift/repository/ProductDBRepository.java @@ -79,13 +79,12 @@ public Optional deleteById(Long id) { private RowMapper productRowMapper() { return (rs, rowNum) -> { - Product product = new Product( + return new Product( rs.getLong("id"), rs.getString("name"), rs.getLong("price"), rs.getString("imageUrl") ); - return product; }; } } From 12cb6f45b6a599529c2a0d6cd5a5ca4ba8cba3ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Thu, 4 Jul 2024 21:29:24 +0900 Subject: [PATCH 09/49] =?UTF-8?q?fix:=20test=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 53 +++++++++++++++++++ .../gift/repository/ProductDBRepository.java | 14 +++-- .../controller/ProductControllerTest.java | 4 +- .../java/gift/service/ProductServiceTest.java | 44 ++++----------- 4 files changed, 72 insertions(+), 43 deletions(-) create mode 100644 src/main/java/gift/exception/GlobalExceptionHandler.java diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..9b8db91ed --- /dev/null +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -0,0 +1,53 @@ +package gift.exception; + +import org.springframework.dao.DataIntegrityViolationException; +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.HashMap; +import java.util.Map; +import java.util.NoSuchElementException; + +@ControllerAdvice +public class GlobalExceptionHandler { + + // 일반적인 예외 처리 + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception ex) { + return new ResponseEntity<>("An error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + + // 데이터 무결성 위반 예외 처리 + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex) { + return new ResponseEntity<>("Data integrity violation: " + ex.getMessage(), HttpStatus.BAD_REQUEST); + } + + // 유효성 검사 실패 예외 처리 + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); + } + + // 요소가 존재하지 않는 예외 처리 + @ExceptionHandler(NoSuchElementException.class) + public ResponseEntity handleNoSuchElementException(NoSuchElementException ex) { + return new ResponseEntity<>("No such element: " + ex.getMessage(), HttpStatus.NOT_FOUND); + } + + // 불법 인수 예외 처리 + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { + return new ResponseEntity<>("Illegal argument: " + ex.getMessage(), HttpStatus.BAD_REQUEST); + } +} diff --git a/src/main/java/gift/repository/ProductDBRepository.java b/src/main/java/gift/repository/ProductDBRepository.java index 0d9fe9f06..78ae899ab 100644 --- a/src/main/java/gift/repository/ProductDBRepository.java +++ b/src/main/java/gift/repository/ProductDBRepository.java @@ -78,13 +78,11 @@ public Optional deleteById(Long id) { } private RowMapper productRowMapper() { - return (rs, rowNum) -> { - return new Product( - rs.getLong("id"), - rs.getString("name"), - rs.getLong("price"), - rs.getString("imageUrl") - ); - }; + return (rs, rowNum) -> new Product( + rs.getLong("id"), + rs.getString("name"), + rs.getLong("price"), + rs.getString("imageUrl") + ); } } diff --git a/src/test/java/gift/controller/ProductControllerTest.java b/src/test/java/gift/controller/ProductControllerTest.java index 975138ed9..c69b77fd9 100644 --- a/src/test/java/gift/controller/ProductControllerTest.java +++ b/src/test/java/gift/controller/ProductControllerTest.java @@ -57,7 +57,7 @@ void getProduct() throws Exception { Product product = new Product(productId, "Test Product", 150L, "test-url"); when(productService.findOne(productId)) - .thenReturn(Optional.of(product)); + .thenReturn(product); mockMvc.perform(MockMvcRequestBuilders.get("/api/products/{id}", productId)) .andExpect(MockMvcResultMatchers.status().isOk()) @@ -93,7 +93,7 @@ void editProductForm() throws Exception { Product product = new Product(productId, "Editable Product", 200L, "edit-url"); when(productService.findOne(productId)) - .thenReturn(Optional.of(product)); + .thenReturn(product); mockMvc.perform(MockMvcRequestBuilders.get("/api/products/edit/{id}", productId)) .andExpect(MockMvcResultMatchers.status().isOk()) diff --git a/src/test/java/gift/service/ProductServiceTest.java b/src/test/java/gift/service/ProductServiceTest.java index f2be7633d..8bf1d1af8 100644 --- a/src/test/java/gift/service/ProductServiceTest.java +++ b/src/test/java/gift/service/ProductServiceTest.java @@ -78,29 +78,19 @@ void findProducts() { } @Test - void findOne_id존재() { + void findOne_id() { Product product = new Product(1L, "Found Product", 150L, "found-product-url"); when(productRepository.findById(1L)) .thenReturn(Optional.of(product)); - Optional foundProduct = productService.findOne(1L); + Product foundProduct = productService.findOne(1L); - assertTrue(foundProduct.isPresent()); - assertEquals(product.getName(), foundProduct.get().getName()); - assertEquals(product.getPrice(), foundProduct.get().getPrice()); - assertEquals(product.getImageUrl(), foundProduct.get().getImageUrl()); - } - - @Test - void findOne_id존재x() { - when(productRepository.findById(999L)) - .thenReturn(Optional.empty()); - - Optional foundProduct = productService.findOne(999L); - - assertFalse(foundProduct.isPresent()); + assertEquals(product.getName(), foundProduct.getName()); + assertEquals(product.getPrice(), foundProduct.getPrice()); + assertEquals(product.getImageUrl(), foundProduct.getImageUrl()); } + @Test void update_id존재() { @@ -134,30 +124,18 @@ void findProducts() { } @Test - void delete_id존재() { + void delete_id() { Long productId = 1L; Product product = new Product(productId, "To Be Deleted", 200L, "delete-me-url"); when(productRepository.deleteById(productId)) .thenReturn(Optional.of(product)); - Optional result = productService.delete(productId); + Product result = productService.delete(productId); - assertTrue(result.isPresent()); - assertEquals(product.getName(), result.get().getName()); - assertEquals(product.getPrice(), result.get().getPrice()); - assertEquals(product.getImageUrl(), result.get().getImageUrl()); + assertEquals(product.getName(), result.getName()); + assertEquals(product.getPrice(), result.getPrice()); + assertEquals(product.getImageUrl(), result.getImageUrl()); } - @Test - void delete_id존재x() { - Long productId = 999L; - - when(productRepository.deleteById(productId)) - .thenReturn(Optional.empty()); - - Optional result = productService.delete(productId); - - assertFalse(result.isPresent()); - } } From ed413713d8b7cf412911aa41f6dfa18fbb9b3f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:09:43 +0900 Subject: [PATCH 10/49] =?UTF-8?q?feat:=20GlobalExceptionHandler=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/controller/ProductRequest.java | 8 ++++---- src/main/java/gift/exception/GlobalExceptionHandler.java | 5 ----- src/test/java/gift/service/ProductServiceTest.java | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main/java/gift/controller/ProductRequest.java b/src/main/java/gift/controller/ProductRequest.java index 5b2c186d9..f5036e40a 100644 --- a/src/main/java/gift/controller/ProductRequest.java +++ b/src/main/java/gift/controller/ProductRequest.java @@ -1,9 +1,7 @@ package gift.controller; import gift.domain.Product; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.*; public class ProductRequest { @@ -16,7 +14,10 @@ public class ProductRequest { ) private String name; + @NotNull(message = "가격을 입력하세요") + @Positive(message = "가격은 양의 정수여야 합니다.") private long price; + private String imageUrl; public ProductRequest(String name, long price, String imageUrl) { @@ -41,7 +42,6 @@ public void setName(String name){ } private boolean isApprovedByMD() { - // MD 승인 여부 확인 로직 (임시로 false 리턴) return false; } diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java index 9b8db91ed..5500ef683 100644 --- a/src/main/java/gift/exception/GlobalExceptionHandler.java +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -15,19 +15,16 @@ @ControllerAdvice public class GlobalExceptionHandler { - // 일반적인 예외 처리 @ExceptionHandler(Exception.class) public ResponseEntity handleException(Exception ex) { return new ResponseEntity<>("An error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); } - // 데이터 무결성 위반 예외 처리 @ExceptionHandler(DataIntegrityViolationException.class) public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex) { return new ResponseEntity<>("Data integrity violation: " + ex.getMessage(), HttpStatus.BAD_REQUEST); } - // 유효성 검사 실패 예외 처리 @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex) { Map errors = new HashMap<>(); @@ -39,13 +36,11 @@ public ResponseEntity> handleValidationExceptions(MethodArgu return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); } - // 요소가 존재하지 않는 예외 처리 @ExceptionHandler(NoSuchElementException.class) public ResponseEntity handleNoSuchElementException(NoSuchElementException ex) { return new ResponseEntity<>("No such element: " + ex.getMessage(), HttpStatus.NOT_FOUND); } - // 불법 인수 예외 처리 @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { return new ResponseEntity<>("Illegal argument: " + ex.getMessage(), HttpStatus.BAD_REQUEST); diff --git a/src/test/java/gift/service/ProductServiceTest.java b/src/test/java/gift/service/ProductServiceTest.java index 8bf1d1af8..6a0ea96f9 100644 --- a/src/test/java/gift/service/ProductServiceTest.java +++ b/src/test/java/gift/service/ProductServiceTest.java @@ -90,7 +90,7 @@ void findOne_id() { assertEquals(product.getPrice(), foundProduct.getPrice()); assertEquals(product.getImageUrl(), foundProduct.getImageUrl()); } - + @Test void update_id존재() { From 1716379db800b0c02b7d932f873e0b61d5e13133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:21:30 +0900 Subject: [PATCH 11/49] =?UTF-8?q?feat:=20InvalidProductDataException,=20Pr?= =?UTF-8?q?oductNotFoundException=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/controller/ProductRequest.java | 5 +++-- .../java/gift/exception/GlobalExceptionHandler.java | 12 ++++++------ .../gift/exception/InvalidProductDataException.java | 11 +++++++++++ .../gift/exception/ProductNotFoundException.java | 7 +++++++ src/main/java/gift/service/ProductService.java | 10 ++++++---- 5 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 src/main/java/gift/exception/InvalidProductDataException.java create mode 100644 src/main/java/gift/exception/ProductNotFoundException.java diff --git a/src/main/java/gift/controller/ProductRequest.java b/src/main/java/gift/controller/ProductRequest.java index f5036e40a..177b35459 100644 --- a/src/main/java/gift/controller/ProductRequest.java +++ b/src/main/java/gift/controller/ProductRequest.java @@ -1,6 +1,7 @@ package gift.controller; import gift.domain.Product; +import gift.exception.InvalidProductDataException; import jakarta.validation.constraints.*; @@ -22,7 +23,7 @@ public class ProductRequest { public ProductRequest(String name, long price, String imageUrl) { if (name.contains("카카오") && !isApprovedByMD()) { - throw new IllegalArgumentException("상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."); + throw new InvalidProductDataException("상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."); } this.name = name; this.price = price; @@ -36,7 +37,7 @@ public String getName(){ } public void setName(String name){ if (name.contains("카카오") && !isApprovedByMD()) { - throw new IllegalArgumentException("상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."); + throw new InvalidProductDataException("상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."); } this.name = name; } diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java index 5500ef683..26d5ad5ab 100644 --- a/src/main/java/gift/exception/GlobalExceptionHandler.java +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -36,13 +36,13 @@ public ResponseEntity> handleValidationExceptions(MethodArgu return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); } - @ExceptionHandler(NoSuchElementException.class) - public ResponseEntity handleNoSuchElementException(NoSuchElementException ex) { - return new ResponseEntity<>("No such element: " + ex.getMessage(), HttpStatus.NOT_FOUND); + @ExceptionHandler(ProductNotFoundException.class) + public ResponseEntity handleProductNotFoundException(ProductNotFoundException ex) { + return new ResponseEntity<>("No such product: " + ex.getMessage(), HttpStatus.NOT_FOUND); } - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity handleIllegalArgumentException(IllegalArgumentException ex) { - return new ResponseEntity<>("Illegal argument: " + ex.getMessage(), HttpStatus.BAD_REQUEST); + @ExceptionHandler(InvalidProductDataException.class) + public ResponseEntity handleInvalidProductDataException(InvalidProductDataException ex) { + return new ResponseEntity<>("Invalid product data: " + ex.getMessage(), HttpStatus.BAD_REQUEST); } } diff --git a/src/main/java/gift/exception/InvalidProductDataException.java b/src/main/java/gift/exception/InvalidProductDataException.java new file mode 100644 index 000000000..029f04c1b --- /dev/null +++ b/src/main/java/gift/exception/InvalidProductDataException.java @@ -0,0 +1,11 @@ +package gift.exception; + +public class InvalidProductDataException extends RuntimeException { + public InvalidProductDataException(String message) { + super(message); + } + + public InvalidProductDataException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/src/main/java/gift/exception/ProductNotFoundException.java b/src/main/java/gift/exception/ProductNotFoundException.java new file mode 100644 index 000000000..824005b33 --- /dev/null +++ b/src/main/java/gift/exception/ProductNotFoundException.java @@ -0,0 +1,7 @@ +package gift.exception; + +public class ProductNotFoundException extends RuntimeException { + public ProductNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java index e85f79121..eb774c528 100644 --- a/src/main/java/gift/service/ProductService.java +++ b/src/main/java/gift/service/ProductService.java @@ -2,6 +2,8 @@ import gift.controller.ProductRequest; import gift.domain.Product; +import gift.exception.InvalidProductDataException; +import gift.exception.ProductNotFoundException; import gift.repository.ProductRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -30,7 +32,7 @@ public Product register(ProductRequest productRequest){ try { return productRepository.save(product); } catch (DataIntegrityViolationException e) { - throw new IllegalArgumentException("상품 데이터가 유효하지 않습니다: " + e.getMessage(), e); + throw new InvalidProductDataException("상품 데이터가 유효하지 않습니다: " + e.getMessage(), e); } } @@ -40,15 +42,15 @@ public List findProducts(){ } public Product findOne(Long productId){ - return productRepository.findById(productId).orElseThrow(() -> new NoSuchElementException("존재하지 않는 상품입니다.")); + return productRepository.findById(productId).orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); } public Product update(Long productId, ProductRequest productRequest){ - return productRepository.updateById(productId, productRequest).orElseThrow(() -> new NoSuchElementException("존재하지 않는 상품입니다.")); + return productRepository.updateById(productId, productRequest).orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); } public Product delete(Long productId){ - return productRepository.deleteById(productId).orElseThrow(() -> new NoSuchElementException("존재하지 않는 상품입니다.")); + return productRepository.deleteById(productId).orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); } } From 39e7ddd7ea98d676b1c32684ec0e055c4712f64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:49:39 +0900 Subject: [PATCH 12/49] =?UTF-8?q?feat:=20validation=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/controller/ProductController.java | 2 +- src/main/java/gift/controller/ProductRequest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/gift/controller/ProductController.java b/src/main/java/gift/controller/ProductController.java index 1132d2abe..3e492ebf8 100644 --- a/src/main/java/gift/controller/ProductController.java +++ b/src/main/java/gift/controller/ProductController.java @@ -49,7 +49,7 @@ public String addProduct(@Valid @ModelAttribute ProductRequest productRequest) { @GetMapping("/api/products/edit/{id}") public String editProductForm(@PathVariable long id, Model model){ Product product = productService.findOne(id); - model.addAttribute("product", ProductRequest.entityToRequest(product)); + model.addAttribute("product", product); return "product-edit-form"; } @PostMapping("/api/products/edit/{id}") diff --git a/src/main/java/gift/controller/ProductRequest.java b/src/main/java/gift/controller/ProductRequest.java index 177b35459..6267c238f 100644 --- a/src/main/java/gift/controller/ProductRequest.java +++ b/src/main/java/gift/controller/ProductRequest.java @@ -10,7 +10,7 @@ public class ProductRequest { @NotBlank(message = "상품 이름은 필수 항목입니다.") @Size(max = 15, message = "상품 이름은 최대 15자까지 입력할 수 있습니다.") @Pattern( - regexp = "^[a-zA-Z0-9()\\[\\]+\\-&/_ ]*$", + regexp = "^[a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ()\\[\\]+\\-&/_ ]*$", message = "상품 이름에 허용되지 않는 특수 문자가 포함되어 있습니다." ) private String name; From 680146d6698c3468dfed3693317076440b460f8b Mon Sep 17 00:00:00 2001 From: Jintaek Jeong <87135698+jjt4515@users.noreply.github.com> Date: Thu, 4 Jul 2024 22:54:40 +0900 Subject: [PATCH 13/49] Update README.md --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4761f39ba..06ed38ffe 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # spring-gift-wishlist + +## step1 + + ### 기능 요구 사항 상품을 추가하거나 수정하는 경우, 클라이언트로부터 잘못된 값이 전달될 수 있다. 잘못된 값이 전달되면 클라이언트가 어떤 부분이 왜 잘못되었는지 인지할 수 있도록 응답을 제공한다. @@ -11,6 +15,6 @@ ### 구현 기능 목록 -- 상품 이름 글자수 제한 -- 특수 문자 제한 -- "카카오"가 포함된 문구 제한 +- 상품 이름 글자수 validation +- 특수 문자 validation +- "카카오"가 포함된 문구 validation From b07581e3bf112ce7308cfe15c598c1398408a433 Mon Sep 17 00:00:00 2001 From: Jintaek Jeong <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 00:22:40 +0900 Subject: [PATCH 14/49] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 41179fe81..615a507e4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ # spring-gift-wishlist -<<<<<<< HEAD - ## step1 - ### 기능 요구 사항 상품을 추가하거나 수정하는 경우, 클라이언트로부터 잘못된 값이 전달될 수 있다. 잘못된 값이 전달되면 클라이언트가 어떤 부분이 왜 잘못되었는지 인지할 수 있도록 응답을 제공한다. @@ -22,4 +19,3 @@ ======= ### 기능 요구 사항 상품 관리 코드를 옮겨 온다. 코드를 옮기는 방법에는 디렉터리의 모든 파일을 직접 복사하여 붙여 넣는 것부터 필요한 일부 파일만 이동하는 것, Git을 사용하는 것까지 여러 가지 방법이 있다. 코드 이동 시 반드시 리소스 파일, 프로퍼티 파일, 테스트 코드 등을 함께 이동한다. ->>>>>>> jjt4515 From 26d234aa033897561d8521c4ffe06d377de82cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 00:44:41 +0900 Subject: [PATCH 15/49] confilct resolve --- .../gift/controller/ProductController.java | 19 ------- .../java/gift/controller/ProductRequest.java | 20 -------- .../gift/repository/ProductDBRepository.java | 25 --------- .../java/gift/service/ProductService.java | 36 ------------- .../controller/ProductControllerTest.java | 8 --- .../java/gift/service/ProductServiceTest.java | 51 +------------------ 6 files changed, 1 insertion(+), 158 deletions(-) diff --git a/src/main/java/gift/controller/ProductController.java b/src/main/java/gift/controller/ProductController.java index 2bc996b9d..3e492ebf8 100644 --- a/src/main/java/gift/controller/ProductController.java +++ b/src/main/java/gift/controller/ProductController.java @@ -2,10 +2,7 @@ import gift.domain.Product; import gift.service.ProductService; -<<<<<<< HEAD import jakarta.validation.Valid; -======= ->>>>>>> jjt4515 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -44,35 +41,19 @@ public String newProductForm(Model model){ } @PostMapping("/api/products") -<<<<<<< HEAD public String addProduct(@Valid @ModelAttribute ProductRequest productRequest) { -======= - public String addProduct(@ModelAttribute ProductRequest productRequest) { ->>>>>>> jjt4515 productService.register(productRequest); return "redirect:/api/products"; } @GetMapping("/api/products/edit/{id}") public String editProductForm(@PathVariable long id, Model model){ -<<<<<<< HEAD Product product = productService.findOne(id); model.addAttribute("product", product); return "product-edit-form"; } @PostMapping("/api/products/edit/{id}") public String updateProduct(@PathVariable Long id, @Valid @ModelAttribute ProductRequest productRequest) { -======= - Optional product = productService.findOne(id); - if (product.isPresent()){ - model.addAttribute("product", product.get()); - return "product-edit-form"; - }; - return "redirect:/api/products"; - } - @PostMapping("/api/products/edit/{id}") - public String updateProduct(@PathVariable Long id, @ModelAttribute ProductRequest productRequest) { ->>>>>>> jjt4515 productService.update(id, productRequest); return "redirect:/api/products"; } diff --git a/src/main/java/gift/controller/ProductRequest.java b/src/main/java/gift/controller/ProductRequest.java index f0ffd2d32..6267c238f 100644 --- a/src/main/java/gift/controller/ProductRequest.java +++ b/src/main/java/gift/controller/ProductRequest.java @@ -1,7 +1,6 @@ package gift.controller; import gift.domain.Product; -<<<<<<< HEAD import gift.exception.InvalidProductDataException; import jakarta.validation.constraints.*; @@ -26,32 +25,17 @@ public ProductRequest(String name, long price, String imageUrl) { if (name.contains("카카오") && !isApprovedByMD()) { throw new InvalidProductDataException("상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."); } -======= - -public class ProductRequest { - private String name; - private long price; - private String imageUrl; - - public ProductRequest(String name, long price, String imageUrl) { ->>>>>>> jjt4515 this.name = name; this.price = price; this.imageUrl = imageUrl; } -<<<<<<< HEAD public ProductRequest() {} -======= - public ProductRequest() { - } ->>>>>>> jjt4515 public String getName(){ return name; } public void setName(String name){ -<<<<<<< HEAD if (name.contains("카카오") && !isApprovedByMD()) { throw new InvalidProductDataException("상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."); } @@ -62,10 +46,6 @@ private boolean isApprovedByMD() { return false; } -======= - this.name = name; - } ->>>>>>> jjt4515 public long getPrice(){ return price; } diff --git a/src/main/java/gift/repository/ProductDBRepository.java b/src/main/java/gift/repository/ProductDBRepository.java index 185a9ff6e..78ae899ab 100644 --- a/src/main/java/gift/repository/ProductDBRepository.java +++ b/src/main/java/gift/repository/ProductDBRepository.java @@ -6,18 +6,12 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.jdbc.support.GeneratedKeyHolder; -<<<<<<< HEAD import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; import javax.sql.DataSource; import java.sql.PreparedStatement; import java.sql.Statement; -======= -import org.springframework.stereotype.Repository; - -import javax.sql.DataSource; ->>>>>>> jjt4515 import java.util.*; @Repository @@ -31,7 +25,6 @@ public ProductDBRepository(DataSource dataSource) { @Override public Product save(Product product) { -<<<<<<< HEAD String sql = "INSERT INTO product(name, price, imageUrl) VALUES (?, ?, ?)"; KeyHolder keyHolder = new GeneratedKeyHolder(); @@ -45,12 +38,6 @@ public Product save(Product product) { }, keyHolder); product.setId(Objects.requireNonNull(keyHolder.getKey()).longValue()); -======= - GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); - String sql = "INSERT INTO product(name, price, imageUrl) VALUES (?, ?, ?)"; - jdbcTemplate.update(sql, product.getName(), product.getPrice(), product.getImageUrl(), keyHolder); - product.setId(keyHolder.getKey().longValue()); ->>>>>>> jjt4515 return product; } @@ -91,23 +78,11 @@ public Optional deleteById(Long id) { } private RowMapper productRowMapper() { -<<<<<<< HEAD return (rs, rowNum) -> new Product( rs.getLong("id"), rs.getString("name"), rs.getLong("price"), rs.getString("imageUrl") ); -======= - return (rs, rowNum) -> { - Product product = new Product( - rs.getLong("id"), - rs.getString("name"), - rs.getLong("price"), - rs.getString("imageUrl") - ); - return product; - }; ->>>>>>> jjt4515 } } diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java index 013345c68..eb774c528 100644 --- a/src/main/java/gift/service/ProductService.java +++ b/src/main/java/gift/service/ProductService.java @@ -2,18 +2,12 @@ import gift.controller.ProductRequest; import gift.domain.Product; -<<<<<<< HEAD import gift.exception.InvalidProductDataException; import gift.exception.ProductNotFoundException; import gift.repository.ProductRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.dao.DataIntegrityViolationException; -======= -import gift.repository.ProductRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; ->>>>>>> jjt4515 import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -34,7 +28,6 @@ public ProductService(ProductRepository productRepository){ } public Product register(ProductRequest productRequest){ -<<<<<<< HEAD Product product = Product.RequestToEntity(productRequest); try { return productRepository.save(product); @@ -42,24 +35,12 @@ public Product register(ProductRequest productRequest){ throw new InvalidProductDataException("상품 데이터가 유효하지 않습니다: " + e.getMessage(), e); } -======= - validateDuplicateProduct(productRequest); - Product product = Product.RequestToEntity(productRequest); - return productRepository.save(product); - } - private void validateDuplicateProduct(ProductRequest productRequest){ - productRepository.findByName(productRequest.getName()) - .ifPresent(p -> { - throw new IllegalStateException("이미 존재하는 상품입니다."); - }); ->>>>>>> jjt4515 } public List findProducts(){ return productRepository.findAll(); } -<<<<<<< HEAD public Product findOne(Long productId){ return productRepository.findById(productId).orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); } @@ -71,22 +52,5 @@ public Product update(Long productId, ProductRequest productRequest){ public Product delete(Long productId){ return productRepository.deleteById(productId).orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); -======= - public Optional findOne(Long productId){ - return productRepository.findById(productId); - } - - public Product update(Long productId, ProductRequest productRequest){ - Optional product = productRepository.updateById(productId, productRequest); - if (product.isPresent()){ - return product.get(); - }; - throw new NoSuchElementException("존재하지 않는 상품입니다."); - - } - - public Optional delete(Long productId){ - return productRepository.deleteById(productId); ->>>>>>> jjt4515 } } diff --git a/src/test/java/gift/controller/ProductControllerTest.java b/src/test/java/gift/controller/ProductControllerTest.java index f3ed31e38..c69b77fd9 100644 --- a/src/test/java/gift/controller/ProductControllerTest.java +++ b/src/test/java/gift/controller/ProductControllerTest.java @@ -57,11 +57,7 @@ void getProduct() throws Exception { Product product = new Product(productId, "Test Product", 150L, "test-url"); when(productService.findOne(productId)) -<<<<<<< HEAD .thenReturn(product); -======= - .thenReturn(Optional.of(product)); ->>>>>>> jjt4515 mockMvc.perform(MockMvcRequestBuilders.get("/api/products/{id}", productId)) .andExpect(MockMvcResultMatchers.status().isOk()) @@ -97,11 +93,7 @@ void editProductForm() throws Exception { Product product = new Product(productId, "Editable Product", 200L, "edit-url"); when(productService.findOne(productId)) -<<<<<<< HEAD .thenReturn(product); -======= - .thenReturn(Optional.of(product)); ->>>>>>> jjt4515 mockMvc.perform(MockMvcRequestBuilders.get("/api/products/edit/{id}", productId)) .andExpect(MockMvcResultMatchers.status().isOk()) diff --git a/src/test/java/gift/service/ProductServiceTest.java b/src/test/java/gift/service/ProductServiceTest.java index 07ce35682..379e6ebb1 100644 --- a/src/test/java/gift/service/ProductServiceTest.java +++ b/src/test/java/gift/service/ProductServiceTest.java @@ -78,17 +78,12 @@ void findProducts() { } @Test -<<<<<<< HEAD void findOne_id() { -======= - void findOne_id존재() { ->>>>>>> jjt4515 Product product = new Product(1L, "Found Product", 150L, "found-product-url"); when(productRepository.findById(1L)) .thenReturn(Optional.of(product)); -<<<<<<< HEAD Product foundProduct = productService.findOne(1L); assertEquals(product.getName(), foundProduct.getName()); @@ -96,25 +91,6 @@ void findOne_id() { assertEquals(product.getImageUrl(), foundProduct.getImageUrl()); } -======= - Optional foundProduct = productService.findOne(1L); - - assertTrue(foundProduct.isPresent()); - assertEquals(product.getName(), foundProduct.get().getName()); - assertEquals(product.getPrice(), foundProduct.get().getPrice()); - assertEquals(product.getImageUrl(), foundProduct.get().getImageUrl()); - } - - @Test - void findOne_id존재x() { - when(productRepository.findById(999L)) - .thenReturn(Optional.empty()); - - Optional foundProduct = productService.findOne(999L); - - assertFalse(foundProduct.isPresent()); - } ->>>>>>> jjt4515 @Test void update_id존재() { @@ -148,18 +124,14 @@ void findOne_id() { } @Test -<<<<<<< HEAD void delete_id() { -======= - void delete_id존재() { ->>>>>>> jjt4515 + Long productId = 1L; Product product = new Product(productId, "To Be Deleted", 200L, "delete-me-url"); when(productRepository.deleteById(productId)) .thenReturn(Optional.of(product)); -<<<<<<< HEAD Product result = productService.delete(productId); assertEquals(product.getName(), result.getName()); @@ -167,25 +139,4 @@ void delete_id() { assertEquals(product.getImageUrl(), result.getImageUrl()); } -======= - Optional result = productService.delete(productId); - - assertTrue(result.isPresent()); - assertEquals(product.getName(), result.get().getName()); - assertEquals(product.getPrice(), result.get().getPrice()); - assertEquals(product.getImageUrl(), result.get().getImageUrl()); - } - - @Test - void delete_id존재x() { - Long productId = 999L; - - when(productRepository.deleteById(productId)) - .thenReturn(Optional.empty()); - - Optional result = productService.delete(productId); - - assertFalse(result.isPresent()); - } ->>>>>>> jjt4515 } From ba19b031b6db0f651c39e88e6ec9e58fc4a9811e Mon Sep 17 00:00:00 2001 From: Jintaek Jeong <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 00:47:40 +0900 Subject: [PATCH 16/49] Update README.md --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 615a507e4..3058beab3 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,3 @@ - 상품 이름 글자수 validation - 특수 문자 validation - "카카오"가 포함된 문구 validation -======= -### 기능 요구 사항 -상품 관리 코드를 옮겨 온다. 코드를 옮기는 방법에는 디렉터리의 모든 파일을 직접 복사하여 붙여 넣는 것부터 필요한 일부 파일만 이동하는 것, Git을 사용하는 것까지 여러 가지 방법이 있다. 코드 이동 시 반드시 리소스 파일, 프로퍼티 파일, 테스트 코드 등을 함께 이동한다. From ac6aeed0a87d5a7fbf690a5bad273768bc3220e3 Mon Sep 17 00:00:00 2001 From: Jintaek Jeong <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:26:02 +0900 Subject: [PATCH 17/49] Update README.md --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3058beab3..68a7e2313 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ ### 구현 기능 목록 -- 상품 이름 글자수 validation -- 특수 문자 validation -- "카카오"가 포함된 문구 validation +- validation + - 상품이름 글자수 최대 15자 + - 상품이름 특수 문자 일부만 사용가능( ), [ ], +, -, &, /, _ + - 상품이름에 "카카오"가 포함된 문구 제한 + - 가격은 양의 정수 +- 예외처리 + - 존재하지 않는 상품인 경우 + - 상품 데이터가 유효하지 않는 경우 From 027c70345f5049f9709854f46d1cc139c5b2ef74 Mon Sep 17 00:00:00 2001 From: Jintaek Jeong <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:48:23 +0900 Subject: [PATCH 18/49] Update README.md --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 68a7e2313..84a59c965 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,17 @@ - 예외처리 - 존재하지 않는 상품인 경우 - 상품 데이터가 유효하지 않는 경우 + +## step2 + +### 기능 요구 사항 +사용자가 회원 가입, 로그인, 추후 회원별 기능을 이용할 수 있도록 구현한다. + +- 회원은 이메일과 비밀번호를 입력하여 가입한다. +- 토큰을 받으려면 이메일과 비밀번호를 보내야 하며, 가입한 이메일과 비밀번호가 일치하면 토큰이 발급된다. +- 토큰을 생성하는 방법에는 여러 가지가 있다. 방법 중 하나를 선택한다. +- (선택) 회원을 조회, 추가, 수정, 삭제할 수 있는 관리자 화면을 구현한다. + +### 구현 기능 목록 +- 회원가입 +- 로그인 From 2a06e9d1866dd79094ebbf53c7c49be5ad0ce3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:47:33 +0900 Subject: [PATCH 19/49] =?UTF-8?q?feat:=20Member=ED=81=B4=EB=9E=98=EC=8A=A4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/domain/Member.java | 39 +++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/gift/domain/Member.java diff --git a/src/main/java/gift/domain/Member.java b/src/main/java/gift/domain/Member.java new file mode 100644 index 000000000..e04bea67c --- /dev/null +++ b/src/main/java/gift/domain/Member.java @@ -0,0 +1,39 @@ +package gift.domain; + +public class Member { + private Long id; + private String email; + private String password; + + public Member() {} + + public Member(Long id, String email, String password) { + this.id = id; + this.email = email; + this.password = password; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} From 81191570266e8434825c967f5d61bc9060e8233c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 12:53:19 +0900 Subject: [PATCH 20/49] =?UTF-8?q?feat:=20MemeberJDBCRepository=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gift/repository/MemberJDBCRepository.java | 48 +++++++++++++++++++ .../gift/repository/MemberRepository.java | 13 +++++ 2 files changed, 61 insertions(+) create mode 100644 src/main/java/gift/repository/MemberJDBCRepository.java create mode 100644 src/main/java/gift/repository/MemberRepository.java diff --git a/src/main/java/gift/repository/MemberJDBCRepository.java b/src/main/java/gift/repository/MemberJDBCRepository.java new file mode 100644 index 000000000..45de565cb --- /dev/null +++ b/src/main/java/gift/repository/MemberJDBCRepository.java @@ -0,0 +1,48 @@ +package gift.repository; + +import gift.domain.Member; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import javax.sql.DataSource; +import java.util.List; +import java.util.Optional; + +@Repository +public class MemberJDBCRepository implements MemberRepository{ + + private final JdbcTemplate jdbcTemplate; + + public MemberJDBCRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public Member save(Member member) { + String sql = "INSERT INTO member (email, password) VALUES (?, ?)"; + jdbcTemplate.update(sql, member.getEmail(), member.getPassword()); + Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class); + member.setId(id); + return member; + } + + public Optional findByEmail(String email) { + String sql = "SELECT * FROM member WHERE email = ?"; + List members = jdbcTemplate.query(sql, memberRowMapper(), email); + return members.stream().findAny(); + } + + public Optional findById(Long id) { + String sql = "SELECT * FROM member WHERE id = ?"; + List members = jdbcTemplate.query(sql, memberRowMapper(), id); + return members.stream().findAny(); + } + + private RowMapper memberRowMapper() { + return (rs, rowNum) -> new Member( + rs.getLong("id"), + rs.getString("email"), + rs.getString("password") + ); + } +} diff --git a/src/main/java/gift/repository/MemberRepository.java b/src/main/java/gift/repository/MemberRepository.java new file mode 100644 index 000000000..f9481ce85 --- /dev/null +++ b/src/main/java/gift/repository/MemberRepository.java @@ -0,0 +1,13 @@ +package gift.repository; + +import gift.domain.Member; + +import java.util.Optional; + +public interface MemberRepository { + Member save(Member member); + Optional findByEmail(String email); + Optional findById(Long id); + + +} From 58ab5299d519dc5d47f4dbd81c1695ba5e8f560f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:09:40 +0900 Subject: [PATCH 21/49] =?UTF-8?q?feat:=20MemberService=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++ src/main/java/gift/config/SecurityConfig.java | 18 ++++++++++ .../java/gift/controller/MemberRequest.java | 36 +++++++++++++++++++ src/main/java/gift/domain/Member.java | 4 +++ src/main/java/gift/service/MemberService.java | 31 ++++++++++++++++ 5 files changed, 94 insertions(+) create mode 100644 src/main/java/gift/config/SecurityConfig.java create mode 100644 src/main/java/gift/controller/MemberRequest.java create mode 100644 src/main/java/gift/service/MemberService.java diff --git a/build.gradle b/build.gradle index 88dba29aa..f8861209e 100644 --- a/build.gradle +++ b/build.gradle @@ -22,9 +22,14 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' + hibernate-core runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + compileOnly 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' } diff --git a/src/main/java/gift/config/SecurityConfig.java b/src/main/java/gift/config/SecurityConfig.java new file mode 100644 index 000000000..1bedd5c3e --- /dev/null +++ b/src/main/java/gift/config/SecurityConfig.java @@ -0,0 +1,18 @@ +package gift.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +} \ No newline at end of file diff --git a/src/main/java/gift/controller/MemberRequest.java b/src/main/java/gift/controller/MemberRequest.java new file mode 100644 index 000000000..9722d3231 --- /dev/null +++ b/src/main/java/gift/controller/MemberRequest.java @@ -0,0 +1,36 @@ +package gift.controller; + +import jakarta.validation.constraints.*; + +public class MemberRequest { + + @NotBlank(message = "이메일은 필수 항목입니다.") + @Email(message = "유효한 이메일 주소를 입력하세요.") + private String email; + + @NotBlank(message = "비밀번호는 필수 항목입니다.") + private String password; + + public MemberRequest() {} + + public MemberRequest(String email, String password) { + this.email = email; + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/gift/domain/Member.java b/src/main/java/gift/domain/Member.java index e04bea67c..cd9cb05f3 100644 --- a/src/main/java/gift/domain/Member.java +++ b/src/main/java/gift/domain/Member.java @@ -1,6 +1,10 @@ package gift.domain; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + public class Member { + private Long id; private String email; private String password; diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java new file mode 100644 index 000000000..6af88e4bc --- /dev/null +++ b/src/main/java/gift/service/MemberService.java @@ -0,0 +1,31 @@ +package gift.service; + +import gift.domain.Member; +import gift.repository.MemberRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class MemberService { + + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; + + @Autowired + public MemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) { + this.memberRepository = memberRepository; + this.passwordEncoder = passwordEncoder; + } + + public Member register(Member member) { + member.setPassword(passwordEncoder.encode(member.getPassword())); + return memberRepository.save(member); + } + + public Optional findByEmail(String email) { + return memberRepository.findByEmail(email); + } +} From 4d600caaae29c14c143a20075e59e476e5e66fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:17:21 +0900 Subject: [PATCH 22/49] =?UTF-8?q?feat:=20CustomUserDetailsService=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CustomUserDetailsService.java | 31 +++++++++++++++++++ src/main/java/gift/util/JwtUtil.java | 29 +++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/main/java/gift/service/CustomUserDetailsService.java create mode 100644 src/main/java/gift/util/JwtUtil.java diff --git a/src/main/java/gift/service/CustomUserDetailsService.java b/src/main/java/gift/service/CustomUserDetailsService.java new file mode 100644 index 000000000..a0b02cd6e --- /dev/null +++ b/src/main/java/gift/service/CustomUserDetailsService.java @@ -0,0 +1,31 @@ +package gift.service; + +import gift.domain.Member; +import gift.repository.MemberRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Autowired + public CustomUserDetailsService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email)); + return org.springframework.security.core.userdetails.User.builder() + .username(member.getEmail()) + .password(member.getPassword()) + .roles("USER") + .build(); + } +} diff --git a/src/main/java/gift/util/JwtUtil.java b/src/main/java/gift/util/JwtUtil.java new file mode 100644 index 000000000..f8b4c43ed --- /dev/null +++ b/src/main/java/gift/util/JwtUtil.java @@ -0,0 +1,29 @@ +package gift.util; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtUtil { + @Value("${jwt.secret}") + private String secret; + + private Key getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes()); + } + + public String generateToken(String email) { + return Jwts.builder() + .setSubject(email) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 hours + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } +} From 9f806012a89c4422f474ceb6866187233f1d1358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:24:28 +0900 Subject: [PATCH 23/49] =?UTF-8?q?feat:=20MemberController=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gift/controller/MemberController.java | 38 +++++++++++++++++++ src/main/java/gift/service/MemberService.java | 27 ++++++++----- 2 files changed, 56 insertions(+), 9 deletions(-) create mode 100644 src/main/java/gift/controller/MemberController.java diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java new file mode 100644 index 000000000..0076ad27e --- /dev/null +++ b/src/main/java/gift/controller/MemberController.java @@ -0,0 +1,38 @@ +package gift.controller; + +import gift.service.MemberService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/members") +public class MemberController { + + private final MemberService memberService; + + @Autowired + public MemberController(MemberService memberService) { + this.memberService = memberService; + } + + @PostMapping("/register") + public ResponseEntity register(@RequestBody Map request) { + String email = request.get("email"); + String password = request.get("password"); + memberService.register(email, password); + return ResponseEntity.ok("회원 가입이 완료되었습니다."); + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody Map request) { + String email = request.get("email"); + String password = request.get("password"); + String token = memberService.authenticate(email, password); + return ResponseEntity.ok(token); + } + +} diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java index 6af88e4bc..f67f4cdbc 100644 --- a/src/main/java/gift/service/MemberService.java +++ b/src/main/java/gift/service/MemberService.java @@ -1,31 +1,40 @@ package gift.service; import gift.domain.Member; +import gift.exception.InvalidCredentialsException; +import gift.exception.MemberNotFoundException; import gift.repository.MemberRepository; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Optional; @Service +@Transactional public class MemberService { private final MemberRepository memberRepository; - private final PasswordEncoder passwordEncoder; @Autowired - public MemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) { + public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; - this.passwordEncoder = passwordEncoder; } - public Member register(Member member) { - member.setPassword(passwordEncoder.encode(member.getPassword())); - return memberRepository.save(member); + public void register(String email, String password) { + Member member = new Member(email, password); + memberRepository.save(member); } - public Optional findByEmail(String email) { - return memberRepository.findByEmail(email); + public String authenticate(String email, String password) { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new MemberNotFoundException("존재하지 않는 회원입니다.")); + + if (!member.getPassword().equals(password)) { + throw new InvalidCredentialsException("잘못된 비밀번호입니다."); + } + + // 실제로는 JWT 토큰을 생성하여 반환해야 하지만, 예시에서는 단순 문자열을 반환하도록 하였습니다. + return "Authentication successful for " + email; } } From dcaa8488193584edcfc591b4182af396adf4d4f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:36:10 +0900 Subject: [PATCH 24/49] =?UTF-8?q?feat:=20Member=20Service=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/config/SecurityConfig.java | 18 ------ .../gift/controller/MemberController.java | 15 ++++- .../exception/GlobalExceptionHandler.java | 12 ++++ .../InvalidCredentialsException.java | 8 +++ .../exception/MemberNotFoundException.java | 8 +++ src/main/java/gift/service/MemberService.java | 57 +++++++++++++------ src/main/java/gift/util/JwtUtil.java | 29 ---------- 7 files changed, 80 insertions(+), 67 deletions(-) delete mode 100644 src/main/java/gift/config/SecurityConfig.java create mode 100644 src/main/java/gift/exception/InvalidCredentialsException.java create mode 100644 src/main/java/gift/exception/MemberNotFoundException.java delete mode 100644 src/main/java/gift/util/JwtUtil.java diff --git a/src/main/java/gift/config/SecurityConfig.java b/src/main/java/gift/config/SecurityConfig.java deleted file mode 100644 index 1bedd5c3e..000000000 --- a/src/main/java/gift/config/SecurityConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -package gift.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; - -@Configuration -public class SecurityConfig { - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - -} \ No newline at end of file diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java index 0076ad27e..86dbdb4de 100644 --- a/src/main/java/gift/controller/MemberController.java +++ b/src/main/java/gift/controller/MemberController.java @@ -1,18 +1,24 @@ package gift.controller; import gift.service.MemberService; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; import java.util.Map; @RestController -@RequestMapping("/api/members") +@RequestMapping("/members") public class MemberController { private final MemberService memberService; + private final String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; @Autowired public MemberController(MemberService memberService) { @@ -28,11 +34,14 @@ public ResponseEntity register(@RequestBody Map request) } @PostMapping("/login") - public ResponseEntity login(@RequestBody Map request) { + public ResponseEntity> login(@RequestBody Map request) { String email = request.get("email"); String password = request.get("password"); String token = memberService.authenticate(email, password); - return ResponseEntity.ok(token); + + Map response = new HashMap<>(); + response.put("token", token); + return ResponseEntity.ok(response); } } diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java index 26d5ad5ab..ce9a7835e 100644 --- a/src/main/java/gift/exception/GlobalExceptionHandler.java +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -45,4 +45,16 @@ public ResponseEntity handleProductNotFoundException(ProductNotFoundExce public ResponseEntity handleInvalidProductDataException(InvalidProductDataException ex) { return new ResponseEntity<>("Invalid product data: " + ex.getMessage(), HttpStatus.BAD_REQUEST); } + + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity handleMemberNotFoundException(MemberNotFoundException ex) { + return new ResponseEntity<>("로그인 실패: " + ex.getMessage(), HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(InvalidCredentialsException.class) + public ResponseEntity handleInvalidCredentialsException(InvalidCredentialsException ex) { + return new ResponseEntity<>("로그인 실패: " + ex.getMessage(), HttpStatus.UNAUTHORIZED); + } + + } diff --git a/src/main/java/gift/exception/InvalidCredentialsException.java b/src/main/java/gift/exception/InvalidCredentialsException.java new file mode 100644 index 000000000..87a486adb --- /dev/null +++ b/src/main/java/gift/exception/InvalidCredentialsException.java @@ -0,0 +1,8 @@ +package gift.exception; + +public class InvalidCredentialsException extends RuntimeException { + + public InvalidCredentialsException(String message) { + super(message); + } +} diff --git a/src/main/java/gift/exception/MemberNotFoundException.java b/src/main/java/gift/exception/MemberNotFoundException.java new file mode 100644 index 000000000..19042fb1a --- /dev/null +++ b/src/main/java/gift/exception/MemberNotFoundException.java @@ -0,0 +1,8 @@ +package gift.exception; + +public class MemberNotFoundException extends RuntimeException { + + public MemberNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java index f67f4cdbc..16a81137b 100644 --- a/src/main/java/gift/service/MemberService.java +++ b/src/main/java/gift/service/MemberService.java @@ -1,40 +1,63 @@ package gift.service; -import gift.domain.Member; import gift.exception.InvalidCredentialsException; import gift.exception.MemberNotFoundException; -import gift.repository.MemberRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Base64; @Service -@Transactional public class MemberService { - private final MemberRepository memberRepository; + private final DataSource dataSource; @Autowired - public MemberService(MemberRepository memberRepository) { - this.memberRepository = memberRepository; + public MemberService(DataSource dataSource) { + this.dataSource = dataSource; } public void register(String email, String password) { - Member member = new Member(email, password); - memberRepository.save(member); + try (Connection conn = dataSource.getConnection()) { + String sql = "INSERT INTO member(email, password) VALUES (?, ?)"; + try (PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, email); + stmt.setString(2, password); + stmt.executeUpdate(); + } + } catch (SQLException e) { + throw new RuntimeException("회원 가입 중 오류가 발생했습니다.", e); + } } public String authenticate(String email, String password) { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new MemberNotFoundException("존재하지 않는 회원입니다.")); - - if (!member.getPassword().equals(password)) { - throw new InvalidCredentialsException("잘못된 비밀번호입니다."); + String sql = "SELECT * FROM member WHERE email = ?"; + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, email); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + String storedPassword = rs.getString("password"); + if (password.equals(storedPassword)) { + return generateJwtToken(email); + } else { + throw new InvalidCredentialsException("잘못된 비밀번호입니다."); + } + } else { + throw new MemberNotFoundException("존재하지 않는 회원입니다."); + } + } + } catch (SQLException e) { + throw new RuntimeException("로그인 중 오류가 발생했습니다.", e); } + } - // 실제로는 JWT 토큰을 생성하여 반환해야 하지만, 예시에서는 단순 문자열을 반환하도록 하였습니다. - return "Authentication successful for " + email; + private String generateJwtToken(String email) { + return email; } } diff --git a/src/main/java/gift/util/JwtUtil.java b/src/main/java/gift/util/JwtUtil.java deleted file mode 100644 index f8b4c43ed..000000000 --- a/src/main/java/gift/util/JwtUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package gift.util; - -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.security.Key; -import java.util.Date; - -@Component -public class JwtUtil { - @Value("${jwt.secret}") - private String secret; - - private Key getSigningKey() { - return Keys.hmacShaKeyFor(secret.getBytes()); - } - - public String generateToken(String email) { - return Jwts.builder() - .setSubject(email) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10 hours - .signWith(getSigningKey(), SignatureAlgorithm.HS256) - .compact(); - } -} From c837e342600d3a338dfa4c2348f1d2344956d1d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:04:43 +0900 Subject: [PATCH 25/49] =?UTF-8?q?feat:=20MemberController,=20MemberService?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 - .../gift/controller/MemberController.java | 47 ++++++++++--------- src/main/java/gift/service/MemberService.java | 13 ++++- 3 files changed, 35 insertions(+), 26 deletions(-) diff --git a/build.gradle b/build.gradle index f8861209e..622f5f5b4 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' - hibernate-core runtimeOnly 'com.h2database:h2' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java index 86dbdb4de..f69f6d61f 100644 --- a/src/main/java/gift/controller/MemberController.java +++ b/src/main/java/gift/controller/MemberController.java @@ -1,47 +1,48 @@ package gift.controller; import gift.service.MemberService; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.HashMap; -import java.util.Map; - @RestController -@RequestMapping("/members") public class MemberController { private final MemberService memberService; - private final String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; @Autowired public MemberController(MemberService memberService) { this.memberService = memberService; } - @PostMapping("/register") - public ResponseEntity register(@RequestBody Map request) { - String email = request.get("email"); - String password = request.get("password"); - memberService.register(email, password); - return ResponseEntity.ok("회원 가입이 완료되었습니다."); + @PostMapping("/members/register") + public ResponseEntity register(@RequestBody MemberRequest memberRequest) { + String token = memberService.register(memberRequest.getEmail(), memberRequest.getPassword()); + TokenResponse response = new TokenResponse(token); + return ResponseEntity.ok(response); } - @PostMapping("/login") - public ResponseEntity> login(@RequestBody Map request) { - String email = request.get("email"); - String password = request.get("password"); - String token = memberService.authenticate(email, password); - - Map response = new HashMap<>(); - response.put("token", token); + @PostMapping("/members/login") + public ResponseEntity login(@RequestBody MemberRequest memberRequest) { + String token = memberService.authenticate(memberRequest.getEmail(), memberRequest.getPassword()); + TokenResponse response = new TokenResponse(token); return ResponseEntity.ok(response); } + private static class TokenResponse { + private String token; + + public TokenResponse(String token) { + this.token = token; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + } } diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java index 16a81137b..cf20949d1 100644 --- a/src/main/java/gift/service/MemberService.java +++ b/src/main/java/gift/service/MemberService.java @@ -2,9 +2,13 @@ import gift.exception.InvalidCredentialsException; import gift.exception.MemberNotFoundException; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import javax.crypto.SecretKey; import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; @@ -16,13 +20,14 @@ public class MemberService { private final DataSource dataSource; + private final SecretKey secretKey = Keys.hmacShaKeyFor("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes()); @Autowired public MemberService(DataSource dataSource) { this.dataSource = dataSource; } - public void register(String email, String password) { + public String register(String email, String password) { try (Connection conn = dataSource.getConnection()) { String sql = "INSERT INTO member(email, password) VALUES (?, ?)"; try (PreparedStatement stmt = conn.prepareStatement(sql)) { @@ -30,6 +35,7 @@ public void register(String email, String password) { stmt.setString(2, password); stmt.executeUpdate(); } + return generateJwtToken(email); } catch (SQLException e) { throw new RuntimeException("회원 가입 중 오류가 발생했습니다.", e); } @@ -58,6 +64,9 @@ public String authenticate(String email, String password) { } private String generateJwtToken(String email) { - return email; + return Jwts.builder() + .setSubject(email) + .signWith(secretKey) + .compact(); } } From 3d8bab41a4ba1fcd8ddad86b6dc7ccff672c4697 Mon Sep 17 00:00:00 2001 From: Jintaek Jeong <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:08:27 +0900 Subject: [PATCH 26/49] Update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 84a59c965..89f608212 100644 --- a/README.md +++ b/README.md @@ -33,5 +33,9 @@ - (선택) 회원을 조회, 추가, 수정, 삭제할 수 있는 관리자 화면을 구현한다. ### 구현 기능 목록 -- 회원가입 -- 로그인 +- 멤버 회원가입 + - 토큰 반환 + - 예외 처리 +- 멤버 로그인 + - 토큰 반환 + - 예외 처리 From 48acd30072b6f105824c34ff0d92f6710911c4f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:41:28 +0900 Subject: [PATCH 27/49] =?UTF-8?q?feat:=20AccessDeniedException=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gift/controller/MemberController.java | 4 +- src/main/java/gift/domain/Member.java | 25 ++++++- .../gift/exception/AccessDeniedException.java | 8 +++ .../exception/GlobalExceptionHandler.java | 5 ++ .../gift/repository/MemberJDBCRepository.java | 13 ++-- .../service/CustomUserDetailsService.java | 31 --------- src/main/java/gift/service/MemberService.java | 66 +++++++------------ 7 files changed, 73 insertions(+), 79 deletions(-) create mode 100644 src/main/java/gift/exception/AccessDeniedException.java delete mode 100644 src/main/java/gift/service/CustomUserDetailsService.java diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java index f69f6d61f..faefb3128 100644 --- a/src/main/java/gift/controller/MemberController.java +++ b/src/main/java/gift/controller/MemberController.java @@ -18,14 +18,14 @@ public MemberController(MemberService memberService) { @PostMapping("/members/register") public ResponseEntity register(@RequestBody MemberRequest memberRequest) { - String token = memberService.register(memberRequest.getEmail(), memberRequest.getPassword()); + String token = memberService.register(memberRequest); TokenResponse response = new TokenResponse(token); return ResponseEntity.ok(response); } @PostMapping("/members/login") public ResponseEntity login(@RequestBody MemberRequest memberRequest) { - String token = memberService.authenticate(memberRequest.getEmail(), memberRequest.getPassword()); + String token = memberService.authenticate(memberRequest); TokenResponse response = new TokenResponse(token); return ResponseEntity.ok(response); } diff --git a/src/main/java/gift/domain/Member.java b/src/main/java/gift/domain/Member.java index cd9cb05f3..fceb23a35 100644 --- a/src/main/java/gift/domain/Member.java +++ b/src/main/java/gift/domain/Member.java @@ -3,18 +3,28 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import java.util.UUID; + public class Member { private Long id; private String email; private String password; + private String secretKey; public Member() {} - public Member(Long id, String email, String password) { + public Member(Long id, String email, String password, String secretKey) { this.id = id; this.email = email; this.password = password; + this.secretKey = secretKey; + } + + public Member(String email, String password) { + this.email = email; + this.password = password; + this.secretKey = generateSecretKey(); } public Long getId() { @@ -40,4 +50,17 @@ public String getPassword() { public void setPassword(String password) { this.password = password; } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + private String generateSecretKey() { + return UUID.randomUUID().toString(); + } + } diff --git a/src/main/java/gift/exception/AccessDeniedException.java b/src/main/java/gift/exception/AccessDeniedException.java new file mode 100644 index 000000000..e5a83c679 --- /dev/null +++ b/src/main/java/gift/exception/AccessDeniedException.java @@ -0,0 +1,8 @@ +package gift.exception; + +public class AccessDeniedException extends RuntimeException { + + public AccessDeniedException(String message) { + super(message); + } +} diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java index ce9a7835e..9581675b4 100644 --- a/src/main/java/gift/exception/GlobalExceptionHandler.java +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -56,5 +56,10 @@ public ResponseEntity handleInvalidCredentialsException(InvalidCredentia return new ResponseEntity<>("로그인 실패: " + ex.getMessage(), HttpStatus.UNAUTHORIZED); } + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(AccessDeniedException ex) { + return new ResponseEntity<>("접근이 거부되었습니다: " + ex.getMessage(), HttpStatus.FORBIDDEN); + } + } diff --git a/src/main/java/gift/repository/MemberJDBCRepository.java b/src/main/java/gift/repository/MemberJDBCRepository.java index 45de565cb..fa51403db 100644 --- a/src/main/java/gift/repository/MemberJDBCRepository.java +++ b/src/main/java/gift/repository/MemberJDBCRepository.java @@ -1,6 +1,7 @@ package gift.repository; import gift.domain.Member; +import gift.exception.MemberNotFoundException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; @@ -10,7 +11,7 @@ import java.util.Optional; @Repository -public class MemberJDBCRepository implements MemberRepository{ +public class MemberJDBCRepository implements MemberRepository { private final JdbcTemplate jdbcTemplate; @@ -18,20 +19,23 @@ public MemberJDBCRepository(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } + @Override public Member save(Member member) { - String sql = "INSERT INTO member (email, password) VALUES (?, ?)"; - jdbcTemplate.update(sql, member.getEmail(), member.getPassword()); + String sql = "INSERT INTO member (email, password, secret_key) VALUES (?, ?, ?)"; + jdbcTemplate.update(sql, member.getEmail(), member.getPassword(), member.getSecretKey()); Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class); member.setId(id); return member; } + @Override public Optional findByEmail(String email) { String sql = "SELECT * FROM member WHERE email = ?"; List members = jdbcTemplate.query(sql, memberRowMapper(), email); return members.stream().findAny(); } + @Override public Optional findById(Long id) { String sql = "SELECT * FROM member WHERE id = ?"; List members = jdbcTemplate.query(sql, memberRowMapper(), id); @@ -42,7 +46,8 @@ private RowMapper memberRowMapper() { return (rs, rowNum) -> new Member( rs.getLong("id"), rs.getString("email"), - rs.getString("password") + rs.getString("password"), + rs.getString("secret_key") ); } } diff --git a/src/main/java/gift/service/CustomUserDetailsService.java b/src/main/java/gift/service/CustomUserDetailsService.java deleted file mode 100644 index a0b02cd6e..000000000 --- a/src/main/java/gift/service/CustomUserDetailsService.java +++ /dev/null @@ -1,31 +0,0 @@ -package gift.service; - -import gift.domain.Member; -import gift.repository.MemberRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -@Service -public class CustomUserDetailsService implements UserDetailsService { - - private final MemberRepository memberRepository; - - @Autowired - public CustomUserDetailsService(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - - @Override - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - Member member = memberRepository.findByEmail(email) - .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + email)); - return org.springframework.security.core.userdetails.User.builder() - .username(member.getEmail()) - .password(member.getPassword()) - .roles("USER") - .build(); - } -} diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java index cf20949d1..59908fea4 100644 --- a/src/main/java/gift/service/MemberService.java +++ b/src/main/java/gift/service/MemberService.java @@ -1,7 +1,11 @@ package gift.service; +import gift.controller.MemberRequest; +import gift.domain.Member; +import gift.exception.AccessDeniedException; import gift.exception.InvalidCredentialsException; import gift.exception.MemberNotFoundException; +import gift.repository.MemberJDBCRepository; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; @@ -9,63 +13,43 @@ import org.springframework.stereotype.Service; import javax.crypto.SecretKey; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.Base64; +import java.util.Optional; @Service public class MemberService { - private final DataSource dataSource; - private final SecretKey secretKey = Keys.hmacShaKeyFor("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes()); + private final MemberJDBCRepository memberRepository; @Autowired - public MemberService(DataSource dataSource) { - this.dataSource = dataSource; + public MemberService(MemberJDBCRepository memberRepository) { + this.memberRepository = memberRepository; } - public String register(String email, String password) { - try (Connection conn = dataSource.getConnection()) { - String sql = "INSERT INTO member(email, password) VALUES (?, ?)"; - try (PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setString(1, email); - stmt.setString(2, password); - stmt.executeUpdate(); - } - return generateJwtToken(email); - } catch (SQLException e) { - throw new RuntimeException("회원 가입 중 오류가 발생했습니다.", e); + public String register(MemberRequest memberRequest) { + Optional oldMember = memberRepository.findByEmail(memberRequest.getEmail()); + if (oldMember.isPresent()) { + throw new AccessDeniedException("이미 등록된 이메일입니다."); } + Member member = new Member(memberRequest.getEmail(), memberRequest.getPassword()); + memberRepository.save(member); + return generateJwtToken(member); } - public String authenticate(String email, String password) { - String sql = "SELECT * FROM member WHERE email = ?"; - try (Connection conn = dataSource.getConnection(); - PreparedStatement stmt = conn.prepareStatement(sql)) { - stmt.setString(1, email); - try (ResultSet rs = stmt.executeQuery()) { - if (rs.next()) { - String storedPassword = rs.getString("password"); - if (password.equals(storedPassword)) { - return generateJwtToken(email); - } else { - throw new InvalidCredentialsException("잘못된 비밀번호입니다."); - } - } else { - throw new MemberNotFoundException("존재하지 않는 회원입니다."); - } - } - } catch (SQLException e) { - throw new RuntimeException("로그인 중 오류가 발생했습니다.", e); + public String authenticate(MemberRequest memberRequest) { + Member member = memberRepository.findByEmail(memberRequest.getEmail()) + .orElseThrow(() -> new MemberNotFoundException("존재하지 않는 회원입니다.")); + + if (!memberRequest.getPassword().equals(member.getPassword())) { + throw new InvalidCredentialsException("잘못된 비밀번호입니다."); } + return generateJwtToken(member); } - private String generateJwtToken(String email) { + private String generateJwtToken(Member member) { + SecretKey secretKey = Keys.hmacShaKeyFor(member.getSecretKey().getBytes()); return Jwts.builder() - .setSubject(email) + .setSubject(member.getEmail()) .signWith(secretKey) .compact(); } From 5a001e9c1770f72a92ac287e0e86e995e0bbd99a Mon Sep 17 00:00:00 2001 From: Jintaek Jeong <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 15:51:24 +0900 Subject: [PATCH 28/49] Update README.md --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 89f608212..1a962b4f8 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,18 @@ - 멤버 로그인 - 토큰 반환 - 예외 처리 + +## step3 + +### 기능 요구 사항 +이전 단계에서 로그인 후 받은 토큰을 사용하여 사용자별 위시 리스트 기능을 구현한다. + +- 위시 리스트에 등록된 상품 목록을 조회할 수 있다. +- 위시 리스트에 상품을 추가할 수 있다. +- 위시 리스트에 담긴 상품을 삭제할 수 있다. + +### 구현 기능 목록 +- 멤버별 위시 리스트 + - 상품 목록 조회 + - 상품 추가 + - 상품 삭제 From 26fd667b6f72a9e3b622e57d4357abf5872e5de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:32:32 +0900 Subject: [PATCH 29/49] =?UTF-8?q?feat:=20WishlistItem=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/domain/Member.java | 1 + src/main/java/gift/domain/WishlistItem.java | 38 +++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/main/java/gift/domain/WishlistItem.java diff --git a/src/main/java/gift/domain/Member.java b/src/main/java/gift/domain/Member.java index fceb23a35..2e15db7d4 100644 --- a/src/main/java/gift/domain/Member.java +++ b/src/main/java/gift/domain/Member.java @@ -63,4 +63,5 @@ private String generateSecretKey() { return UUID.randomUUID().toString(); } + } diff --git a/src/main/java/gift/domain/WishlistItem.java b/src/main/java/gift/domain/WishlistItem.java new file mode 100644 index 000000000..6e69d6110 --- /dev/null +++ b/src/main/java/gift/domain/WishlistItem.java @@ -0,0 +1,38 @@ +package gift.domain; + +public class WishlistItem { + + private Long id; + private Long memberId; + private String itemName; + + public WishlistItem(Long id, Long memberId, String itemName) { + this.id = id; + this.memberId = memberId; + this.itemName = itemName; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } + + public String getItemName() { + return itemName; + } + + public void setItemName(String itemName) { + this.itemName = itemName; + } +} From b6c5192ffee399ea049c73c46463cf56bbd1f502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:35:20 +0900 Subject: [PATCH 30/49] =?UTF-8?q?feat:=20WishlistJDBCRepository=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/WishlistJDBCRepository.java | 42 +++++++++++++++++++ .../gift/repository/WishlistRepository.java | 11 +++++ 2 files changed, 53 insertions(+) create mode 100644 src/main/java/gift/repository/WishlistJDBCRepository.java create mode 100644 src/main/java/gift/repository/WishlistRepository.java diff --git a/src/main/java/gift/repository/WishlistJDBCRepository.java b/src/main/java/gift/repository/WishlistJDBCRepository.java new file mode 100644 index 000000000..7497f924e --- /dev/null +++ b/src/main/java/gift/repository/WishlistJDBCRepository.java @@ -0,0 +1,42 @@ +package gift.repository; + +import gift.domain.WishlistItem; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import javax.sql.DataSource; +import java.util.List; + +@Repository +public class WishlistJDBCRepository implements WishlistRepository { + + private final JdbcTemplate jdbcTemplate; + + public WishlistJDBCRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public void addItem(WishlistItem item) { + String sql = "INSERT INTO wishlist (member_id, item_name) VALUES (?, ?)"; + jdbcTemplate.update(sql, item.getMemberId(), item.getItemName()); + } + + public void deleteItem(Long itemId) { + String sql = "DELETE FROM wishlist WHERE id = ?"; + jdbcTemplate.update(sql, itemId); + } + + public List getItemsByMemberId(Long memberId) { + String sql = "SELECT * FROM wishlist WHERE member_id = ?"; + return jdbcTemplate.query(sql, wishlistItemRowMapper(), memberId); + } + + private RowMapper wishlistItemRowMapper() { + return (rs, rowNum) -> new WishlistItem( + rs.getLong("id"), + rs.getLong("member_id"), + rs.getString("item_name") + ); + } +} diff --git a/src/main/java/gift/repository/WishlistRepository.java b/src/main/java/gift/repository/WishlistRepository.java new file mode 100644 index 000000000..9ef93347a --- /dev/null +++ b/src/main/java/gift/repository/WishlistRepository.java @@ -0,0 +1,11 @@ +package gift.repository; + +import gift.domain.WishlistItem; + +import java.util.List; + +public interface WishlistRepository { + void addItem(WishlistItem item); + void deleteItem(Long itemId); + List getItemsByMemberId(Long memberId); +} From aa49dda4e0823dc63d9f47201dc9d44824548484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:49:57 +0900 Subject: [PATCH 31/49] =?UTF-8?q?feat:=20WishlistService=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gift/controller/WishlistIdRequest.java | 32 +++++++++++++ .../gift/controller/WishlistNameRequest.java | 32 +++++++++++++ .../java/gift/service/WishlistService.java | 46 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 src/main/java/gift/controller/WishlistIdRequest.java create mode 100644 src/main/java/gift/controller/WishlistNameRequest.java create mode 100644 src/main/java/gift/service/WishlistService.java diff --git a/src/main/java/gift/controller/WishlistIdRequest.java b/src/main/java/gift/controller/WishlistIdRequest.java new file mode 100644 index 000000000..7193a9d88 --- /dev/null +++ b/src/main/java/gift/controller/WishlistIdRequest.java @@ -0,0 +1,32 @@ +package gift.controller; +import jakarta.validation.constraints.*; + +public class WishlistIdRequest { + + @NotNull(message = "Item ID를 입력하세요") + private Long itemId; + + @NotNull(message = "Member ID를 입력하세요") + private Long memberId; + + public WishlistIdRequest(Long itemId, Long memberId) { + this.itemId = itemId; + this.memberId = memberId; + } + + public Long getItemId() { + return itemId; + } + + public void setItemId(Long itemId) { + this.itemId = itemId; + } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } +} diff --git a/src/main/java/gift/controller/WishlistNameRequest.java b/src/main/java/gift/controller/WishlistNameRequest.java new file mode 100644 index 000000000..6968477ec --- /dev/null +++ b/src/main/java/gift/controller/WishlistNameRequest.java @@ -0,0 +1,32 @@ +package gift.controller; + +import jakarta.validation.constraints.NotNull; + +public class WishlistNameRequest { + + @NotNull(message = "Member ID를 입력하세요") + private Long memberId; + @NotNull(message = "Item Name을 입력하세요") + private String itemName; + + public WishlistNameRequest(Long memberId, String itemName) { + this.memberId = memberId; + this.itemName = itemName; + } + + public Long getMemberId() { + return memberId; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } + + public String getItemName() { + return itemName; + } + + public void setItemName(String itemName) { + this.itemName = itemName; + } +} diff --git a/src/main/java/gift/service/WishlistService.java b/src/main/java/gift/service/WishlistService.java new file mode 100644 index 000000000..3b842808c --- /dev/null +++ b/src/main/java/gift/service/WishlistService.java @@ -0,0 +1,46 @@ +package gift.service; + +import gift.controller.WishlistIdRequest; +import gift.controller.WishlistNameRequest; +import gift.domain.WishlistItem; +import gift.exception.AccessDeniedException; +import gift.exception.MemberNotFoundException; +import gift.repository.WishlistRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class WishlistService { + + private final WishlistRepository wishlistRepository; + + @Autowired + public WishlistService(WishlistRepository wishlistRepository) { + this.wishlistRepository = wishlistRepository; + } + + public void addItemToWishlist(WishlistNameRequest wishlistNameRequest) { + WishlistItem item = new WishlistItem(null, wishlistNameRequest.getMemberId(), wishlistNameRequest.getItemName()); + wishlistRepository.addItem(item); + } + + public void deleteItemFromWishlist(WishlistIdRequest wishlistIdRequest) { + WishlistItem existingItem = wishlistRepository.getItemsByMemberId(wishlistIdRequest.getMemberId()) + .stream() + .filter(item -> item.getId().equals(wishlistIdRequest.getItemId())) + .findFirst() + .orElseThrow(() -> new MemberNotFoundException("Wishlist item not found for memberId: " + wishlistIdRequest.getMemberId())); + + if (!existingItem.getMemberId().equals(wishlistIdRequest.getMemberId())) { + throw new AccessDeniedException("You are not authorized to delete this item."); + } + + wishlistRepository.deleteItem(wishlistIdRequest.getItemId()); + } + + public List getWishlistByMemberId(Long memberId) { + return wishlistRepository.getItemsByMemberId(memberId); + } +} From 5d302bd68926a9be43e6dfc038da07c55696ab83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:57:02 +0900 Subject: [PATCH 32/49] =?UTF-8?q?feat:=20WishlistController=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gift/controller/WishlistController.java | 36 +++++++++++++++++++ .../repository/WishlistJDBCRepository.java | 6 ++-- 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 src/main/java/gift/controller/WishlistController.java diff --git a/src/main/java/gift/controller/WishlistController.java b/src/main/java/gift/controller/WishlistController.java new file mode 100644 index 000000000..395410a8c --- /dev/null +++ b/src/main/java/gift/controller/WishlistController.java @@ -0,0 +1,36 @@ +package gift.controller; + +import gift.domain.WishlistItem; +import gift.service.WishlistService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + + +@RestController +public class WishlistController { + + private final WishlistService wishlistService; + + @Autowired + public WishlistController(WishlistService wishlistService) { + this.wishlistService = wishlistService; + } + + @PostMapping("/wishlist/add") + public void addToWishlist(@Valid @RequestBody WishlistNameRequest request) { + wishlistService.addItemToWishlist(request); + } + + @DeleteMapping("/wishlist/delete") + public void deleteFromWishlist(@Valid @RequestBody WishlistIdRequest request) { + wishlistService.deleteItemFromWishlist(request); + } + + @GetMapping("/wishlist/get/{memberId}") + public List getWishlist(@PathVariable Long memberId) { + return wishlistService.getWishlistByMemberId(memberId); + } +} diff --git a/src/main/java/gift/repository/WishlistJDBCRepository.java b/src/main/java/gift/repository/WishlistJDBCRepository.java index 7497f924e..becbcafd4 100644 --- a/src/main/java/gift/repository/WishlistJDBCRepository.java +++ b/src/main/java/gift/repository/WishlistJDBCRepository.java @@ -16,17 +16,17 @@ public class WishlistJDBCRepository implements WishlistRepository { public WishlistJDBCRepository(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } - + @Override public void addItem(WishlistItem item) { String sql = "INSERT INTO wishlist (member_id, item_name) VALUES (?, ?)"; jdbcTemplate.update(sql, item.getMemberId(), item.getItemName()); } - + @Override public void deleteItem(Long itemId) { String sql = "DELETE FROM wishlist WHERE id = ?"; jdbcTemplate.update(sql, itemId); } - + @Override public List getItemsByMemberId(Long memberId) { String sql = "SELECT * FROM wishlist WHERE member_id = ?"; return jdbcTemplate.query(sql, wishlistItemRowMapper(), memberId); From 54fbd1aa969c96f22e056f49d69d3daedb4fabbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:08:36 +0900 Subject: [PATCH 33/49] =?UTF-8?q?feat:=20LoginMember=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gift/controller/LoginMember.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/main/java/gift/controller/LoginMember.java diff --git a/src/main/java/gift/controller/LoginMember.java b/src/main/java/gift/controller/LoginMember.java new file mode 100644 index 000000000..9eced1999 --- /dev/null +++ b/src/main/java/gift/controller/LoginMember.java @@ -0,0 +1,48 @@ +package gift.controller.; + +public class LoginMember { + private Long id; + private String name; + private String email; + private String role; + + public LoginMember(Long id, String name, String email, String role) { + this.id = id; + this.name = name; + this.email = email; + this.role = role; + } + + // Getters and setters + 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 String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } +} From 3c00aa06d0ae4a22050d50de0dbb00e085398301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:52:41 +0900 Subject: [PATCH 34/49] =?UTF-8?q?feat:=20WishlistJDBCRepository=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gift/controller/LoginMember.java | 48 ------------------- src/main/java/gift/domain/WishlistItem.java | 5 ++ .../repository/WishlistJDBCRepository.java | 2 + .../java/gift/service/WishlistService.java | 6 +-- src/main/resources/schema.sql | 9 ++++ 5 files changed, 19 insertions(+), 51 deletions(-) delete mode 100644 src/main/java/gift/controller/LoginMember.java diff --git a/src/main/java/gift/controller/LoginMember.java b/src/main/java/gift/controller/LoginMember.java deleted file mode 100644 index 9eced1999..000000000 --- a/src/main/java/gift/controller/LoginMember.java +++ /dev/null @@ -1,48 +0,0 @@ -package gift.controller.; - -public class LoginMember { - private Long id; - private String name; - private String email; - private String role; - - public LoginMember(Long id, String name, String email, String role) { - this.id = id; - this.name = name; - this.email = email; - this.role = role; - } - - // Getters and setters - 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 String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getRole() { - return role; - } - - public void setRole(String role) { - this.role = role; - } -} diff --git a/src/main/java/gift/domain/WishlistItem.java b/src/main/java/gift/domain/WishlistItem.java index 6e69d6110..05e154f15 100644 --- a/src/main/java/gift/domain/WishlistItem.java +++ b/src/main/java/gift/domain/WishlistItem.java @@ -12,6 +12,11 @@ public WishlistItem(Long id, Long memberId, String itemName) { this.itemName = itemName; } + public WishlistItem(Long memberId, String itemName) { + this.memberId = memberId; + this.itemName = itemName; + } + public Long getId() { return id; } diff --git a/src/main/java/gift/repository/WishlistJDBCRepository.java b/src/main/java/gift/repository/WishlistJDBCRepository.java index becbcafd4..31bfe825a 100644 --- a/src/main/java/gift/repository/WishlistJDBCRepository.java +++ b/src/main/java/gift/repository/WishlistJDBCRepository.java @@ -20,6 +20,8 @@ public WishlistJDBCRepository(DataSource dataSource) { public void addItem(WishlistItem item) { String sql = "INSERT INTO wishlist (member_id, item_name) VALUES (?, ?)"; jdbcTemplate.update(sql, item.getMemberId(), item.getItemName()); + Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class); + item.setId(id); // WishlistItem 객체에 자동 생성된 id 할당 } @Override public void deleteItem(Long itemId) { diff --git a/src/main/java/gift/service/WishlistService.java b/src/main/java/gift/service/WishlistService.java index 3b842808c..adbdc6be6 100644 --- a/src/main/java/gift/service/WishlistService.java +++ b/src/main/java/gift/service/WishlistService.java @@ -22,7 +22,7 @@ public WishlistService(WishlistRepository wishlistRepository) { } public void addItemToWishlist(WishlistNameRequest wishlistNameRequest) { - WishlistItem item = new WishlistItem(null, wishlistNameRequest.getMemberId(), wishlistNameRequest.getItemName()); + WishlistItem item = new WishlistItem(wishlistNameRequest.getMemberId(), wishlistNameRequest.getItemName()); wishlistRepository.addItem(item); } @@ -31,10 +31,10 @@ public void deleteItemFromWishlist(WishlistIdRequest wishlistIdRequest) { .stream() .filter(item -> item.getId().equals(wishlistIdRequest.getItemId())) .findFirst() - .orElseThrow(() -> new MemberNotFoundException("Wishlist item not found for memberId: " + wishlistIdRequest.getMemberId())); + .orElseThrow(() -> new MemberNotFoundException("위시리스트가 비어있습니다: " + wishlistIdRequest.getMemberId())); if (!existingItem.getMemberId().equals(wishlistIdRequest.getMemberId())) { - throw new AccessDeniedException("You are not authorized to delete this item."); + throw new AccessDeniedException("아이템 삭제 권한이 없습니다."); } wishlistRepository.deleteItem(wishlistIdRequest.getItemId()); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index cca12d009..8c722bed7 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -3,4 +3,13 @@ CREATE TABLE product ( name VARCHAR(255) NOT NULL, price BIGINT NOT NULL, imageUrl VARCHAR(255) +); +CREATE TABLE wishlist ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + member_id BIGINT NOT NULL, + item_name VARCHAR(255) NOT NULL, + CONSTRAINT fk_member + FOREIGN KEY (member_id) + REFERENCES member(id) + ON DELETE CASCADE ); \ No newline at end of file From 2dd7945907a6ad5dd60f3e24dbfa5acc1c6b9006 Mon Sep 17 00:00:00 2001 From: Jintaek Jeong <87135698+jjt4515@users.noreply.github.com> Date: Fri, 5 Jul 2024 17:57:07 +0900 Subject: [PATCH 35/49] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1a962b4f8..750f1ff6d 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ ### 구현 기능 목록 - 멤버별 위시 리스트 - - 상품 목록 조회 - - 상품 추가 - - 상품 삭제 + - 위시 리스트 상품 목록 조회 + - 위시 리스트 상품 추가 + - 위시 리스트 상품 삭제 +- 예외 처리 From 489905362131d904deb664005b164f70d8cd4e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Sat, 6 Jul 2024 00:18:47 +0900 Subject: [PATCH 36/49] =?UTF-8?q?feat:=20schema.sql=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8c722bed7..9d4741cd3 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,3 +1,9 @@ +CREATE TABLE member ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + secretKey VARCHAR(255) NOT NULL +); CREATE TABLE product ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, @@ -12,4 +18,4 @@ CREATE TABLE wishlist ( FOREIGN KEY (member_id) REFERENCES member(id) ON DELETE CASCADE -); \ No newline at end of file +); From 153d72b876b1e21ff8b31983fb2e2275af425035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Sun, 7 Jul 2024 20:58:47 +0900 Subject: [PATCH 37/49] =?UTF-8?q?style:=20ProductService=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/service/ProductService.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java index eb774c528..bb7cf08ae 100644 --- a/src/main/java/gift/service/ProductService.java +++ b/src/main/java/gift/service/ProductService.java @@ -42,15 +42,18 @@ public List findProducts(){ } public Product findOne(Long productId){ - return productRepository.findById(productId).orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); + return productRepository.findById(productId) + .orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); } public Product update(Long productId, ProductRequest productRequest){ - return productRepository.updateById(productId, productRequest).orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); + return productRepository.updateById(productId, productRequest) + .orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); } public Product delete(Long productId){ - return productRepository.deleteById(productId).orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); + return productRepository.deleteById(productId) + .orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); } } From 49b4ec8524406ffa2ad88b5e79dd3124f236f31b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:43:04 +0900 Subject: [PATCH 38/49] =?UTF-8?q?refactor:=20=EB=AA=A8=EB=93=A0=20Request?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gift/controller/MemberRequest.java | 7 ----- .../java/gift/controller/ProductRequest.java | 12 -------- .../gift/controller/WishlistIdRequest.java | 6 ---- .../gift/controller/WishlistNameRequest.java | 8 +---- .../java/gift/exception/ErrorResponse.java | 29 +++++++++++++++++++ 5 files changed, 30 insertions(+), 32 deletions(-) create mode 100644 src/main/java/gift/exception/ErrorResponse.java diff --git a/src/main/java/gift/controller/MemberRequest.java b/src/main/java/gift/controller/MemberRequest.java index 9722d3231..788ef225c 100644 --- a/src/main/java/gift/controller/MemberRequest.java +++ b/src/main/java/gift/controller/MemberRequest.java @@ -22,15 +22,8 @@ public String getEmail() { return email; } - public void setEmail(String email) { - this.email = email; - } - public String getPassword() { return password; } - public void setPassword(String password) { - this.password = password; - } } diff --git a/src/main/java/gift/controller/ProductRequest.java b/src/main/java/gift/controller/ProductRequest.java index 6267c238f..1b8c66933 100644 --- a/src/main/java/gift/controller/ProductRequest.java +++ b/src/main/java/gift/controller/ProductRequest.java @@ -35,12 +35,6 @@ public ProductRequest() {} public String getName(){ return name; } - public void setName(String name){ - if (name.contains("카카오") && !isApprovedByMD()) { - throw new InvalidProductDataException("상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."); - } - this.name = name; - } private boolean isApprovedByMD() { return false; @@ -49,15 +43,9 @@ private boolean isApprovedByMD() { public long getPrice(){ return price; } - public void setPrice(long price){ - this.price = price; - } public String getImageUrl(){ return imageUrl; } - public void setImageUrl(String imageUrl){ - this.imageUrl = imageUrl; - } public static ProductRequest entityToRequest(Product product){ return new ProductRequest(product.getName(), product.getPrice(), product.getImageUrl()); diff --git a/src/main/java/gift/controller/WishlistIdRequest.java b/src/main/java/gift/controller/WishlistIdRequest.java index 7193a9d88..8a7f0361f 100644 --- a/src/main/java/gift/controller/WishlistIdRequest.java +++ b/src/main/java/gift/controller/WishlistIdRequest.java @@ -18,15 +18,9 @@ public Long getItemId() { return itemId; } - public void setItemId(Long itemId) { - this.itemId = itemId; - } public Long getMemberId() { return memberId; } - public void setMemberId(Long memberId) { - this.memberId = memberId; - } } diff --git a/src/main/java/gift/controller/WishlistNameRequest.java b/src/main/java/gift/controller/WishlistNameRequest.java index 6968477ec..2fa94468e 100644 --- a/src/main/java/gift/controller/WishlistNameRequest.java +++ b/src/main/java/gift/controller/WishlistNameRequest.java @@ -18,15 +18,9 @@ public Long getMemberId() { return memberId; } - public void setMemberId(Long memberId) { - this.memberId = memberId; - } public String getItemName() { return itemName; } - - public void setItemName(String itemName) { - this.itemName = itemName; - } + } diff --git a/src/main/java/gift/exception/ErrorResponse.java b/src/main/java/gift/exception/ErrorResponse.java new file mode 100644 index 000000000..e4b45bc16 --- /dev/null +++ b/src/main/java/gift/exception/ErrorResponse.java @@ -0,0 +1,29 @@ +package gift.exception; + +import org.springframework.http.HttpStatus; + +public class ErrorResponse { + private String message; + private HttpStatus status; + + public ErrorResponse(String message, HttpStatus status) { + this.message = message; + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public HttpStatus getStatus() { + return status; + } + + public void setStatus(HttpStatus status) { + this.status = status; + } +} From c94767fd0486c78a41d72203576833f9220cfaa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Mon, 8 Jul 2024 15:59:40 +0900 Subject: [PATCH 39/49] =?UTF-8?q?refactor:=20Exception=20Handler=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gift/controller/WishlistNameRequest.java | 2 +- .../java/gift/exception/ErrorResponse.java | 22 ++++++----- .../exception/GlobalExceptionHandler.java | 39 ++++++++++--------- 3 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/main/java/gift/controller/WishlistNameRequest.java b/src/main/java/gift/controller/WishlistNameRequest.java index 2fa94468e..641cb7296 100644 --- a/src/main/java/gift/controller/WishlistNameRequest.java +++ b/src/main/java/gift/controller/WishlistNameRequest.java @@ -22,5 +22,5 @@ public Long getMemberId() { public String getItemName() { return itemName; } - + } diff --git a/src/main/java/gift/exception/ErrorResponse.java b/src/main/java/gift/exception/ErrorResponse.java index e4b45bc16..ded1e4e38 100644 --- a/src/main/java/gift/exception/ErrorResponse.java +++ b/src/main/java/gift/exception/ErrorResponse.java @@ -1,29 +1,31 @@ package gift.exception; -import org.springframework.http.HttpStatus; +import java.util.Map; public class ErrorResponse { private String message; - private HttpStatus status; + private String status; + private Map errors; - public ErrorResponse(String message, HttpStatus status) { + public ErrorResponse(String message, String status) { this.message = message; this.status = status; } - public String getMessage() { - return message; + public ErrorResponse(Map errors, String status) { + this.errors = errors; + this.status = status; } - public void setMessage(String message) { - this.message = message; + public String getMessage() { + return message; } - public HttpStatus getStatus() { + public String getStatus() { return status; } - public void setStatus(HttpStatus status) { - this.status = status; + public Map getErrors() { + return errors; } } diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java index 9581675b4..4537d91aa 100644 --- a/src/main/java/gift/exception/GlobalExceptionHandler.java +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -10,56 +10,59 @@ import java.util.HashMap; import java.util.Map; -import java.util.NoSuchElementException; @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) - public ResponseEntity handleException(Exception ex) { - return new ResponseEntity<>("An error occurred: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + public ResponseEntity handleException(Exception ex) { + return buildErrorResponse("An error occurred: " + ex.getMessage(), "500", HttpStatus.INTERNAL_SERVER_ERROR); } @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex) { - return new ResponseEntity<>("Data integrity violation: " + ex.getMessage(), HttpStatus.BAD_REQUEST); + public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex) { + return buildErrorResponse("Data integrity violation: " + ex.getMessage(), "400", HttpStatus.BAD_REQUEST); } @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex) { + public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { Map errors = new HashMap<>(); ex.getBindingResult().getAllErrors().forEach((error) -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); - return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST); + ErrorResponse errorResponse = new ErrorResponse(errors, "400"); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); } @ExceptionHandler(ProductNotFoundException.class) - public ResponseEntity handleProductNotFoundException(ProductNotFoundException ex) { - return new ResponseEntity<>("No such product: " + ex.getMessage(), HttpStatus.NOT_FOUND); + public ResponseEntity handleProductNotFoundException(ProductNotFoundException ex) { + return buildErrorResponse("No such product: " + ex.getMessage(), "404", HttpStatus.NOT_FOUND); } @ExceptionHandler(InvalidProductDataException.class) - public ResponseEntity handleInvalidProductDataException(InvalidProductDataException ex) { - return new ResponseEntity<>("Invalid product data: " + ex.getMessage(), HttpStatus.BAD_REQUEST); + public ResponseEntity handleInvalidProductDataException(InvalidProductDataException ex) { + return buildErrorResponse("Invalid product data: " + ex.getMessage(), "400", HttpStatus.BAD_REQUEST); } @ExceptionHandler(MemberNotFoundException.class) - public ResponseEntity handleMemberNotFoundException(MemberNotFoundException ex) { - return new ResponseEntity<>("로그인 실패: " + ex.getMessage(), HttpStatus.UNAUTHORIZED); + public ResponseEntity handleMemberNotFoundException(MemberNotFoundException ex) { + return buildErrorResponse("로그인 실패: " + ex.getMessage(), "401", HttpStatus.UNAUTHORIZED); } @ExceptionHandler(InvalidCredentialsException.class) - public ResponseEntity handleInvalidCredentialsException(InvalidCredentialsException ex) { - return new ResponseEntity<>("로그인 실패: " + ex.getMessage(), HttpStatus.UNAUTHORIZED); + public ResponseEntity handleInvalidCredentialsException(InvalidCredentialsException ex) { + return buildErrorResponse("로그인 실패: " + ex.getMessage(), "401", HttpStatus.UNAUTHORIZED); } @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity handleAccessDeniedException(AccessDeniedException ex) { - return new ResponseEntity<>("접근이 거부되었습니다: " + ex.getMessage(), HttpStatus.FORBIDDEN); + public ResponseEntity handleAccessDeniedException(AccessDeniedException ex) { + return buildErrorResponse("접근이 거부되었습니다: " + ex.getMessage(), "403", HttpStatus.FORBIDDEN); } - + private ResponseEntity buildErrorResponse(String message, String status, HttpStatus httpStatus) { + ErrorResponse errorResponse = new ErrorResponse(message, status); + return new ResponseEntity<>(errorResponse, httpStatus); + } } From 962ce6d45b02e061cec7f871f18496b76bed6662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:39:56 +0900 Subject: [PATCH 40/49] =?UTF-8?q?chore:=20dto=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/controller/MemberController.java | 2 +- src/main/java/gift/controller/ProductController.java | 3 +-- src/main/java/gift/controller/WishlistController.java | 2 ++ src/main/java/gift/domain/Product.java | 2 +- .../gift/{controller => dto/request}/MemberRequest.java | 2 +- .../gift/{controller => dto/request}/ProductRequest.java | 2 +- .../{controller => dto/request}/WishlistIdRequest.java | 2 +- .../{controller => dto/request}/WishlistNameRequest.java | 2 +- .../gift/{exception => dto/response}/ErrorResponse.java | 2 +- src/main/java/gift/exception/GlobalExceptionHandler.java | 1 + src/main/java/gift/repository/ProductDBRepository.java | 3 +-- src/main/java/gift/repository/ProductMemoryRepository.java | 3 +-- src/main/java/gift/repository/ProductRepository.java | 2 +- src/main/java/gift/service/MemberService.java | 4 +--- src/main/java/gift/service/ProductService.java | 7 +------ src/main/java/gift/service/WishlistService.java | 4 ++-- src/test/java/gift/controller/ProductControllerTest.java | 5 +---- src/test/java/gift/repository/ProductDBRepositoryTest.java | 3 +-- .../java/gift/repository/ProductMemoryRepositoryTest.java | 2 +- src/test/java/gift/service/ProductServiceTest.java | 3 +-- 20 files changed, 22 insertions(+), 34 deletions(-) rename src/main/java/gift/{controller => dto/request}/MemberRequest.java (95%) rename src/main/java/gift/{controller => dto/request}/ProductRequest.java (98%) rename src/main/java/gift/{controller => dto/request}/WishlistIdRequest.java (95%) rename src/main/java/gift/{controller => dto/request}/WishlistNameRequest.java (95%) rename src/main/java/gift/{exception => dto/response}/ErrorResponse.java (95%) diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java index faefb3128..13666ad07 100644 --- a/src/main/java/gift/controller/MemberController.java +++ b/src/main/java/gift/controller/MemberController.java @@ -1,8 +1,8 @@ package gift.controller; +import gift.dto.request.MemberRequest; import gift.service.MemberService; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; diff --git a/src/main/java/gift/controller/ProductController.java b/src/main/java/gift/controller/ProductController.java index 3e492ebf8..db324bba0 100644 --- a/src/main/java/gift/controller/ProductController.java +++ b/src/main/java/gift/controller/ProductController.java @@ -1,6 +1,7 @@ package gift.controller; import gift.domain.Product; +import gift.dto.request.ProductRequest; import gift.service.ProductService; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; @@ -8,8 +9,6 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; -import java.util.*; - @Controller //@RequestMapping("/api/products") public class ProductController { diff --git a/src/main/java/gift/controller/WishlistController.java b/src/main/java/gift/controller/WishlistController.java index 395410a8c..0356dafbb 100644 --- a/src/main/java/gift/controller/WishlistController.java +++ b/src/main/java/gift/controller/WishlistController.java @@ -1,6 +1,8 @@ package gift.controller; import gift.domain.WishlistItem; +import gift.dto.request.WishlistIdRequest; +import gift.dto.request.WishlistNameRequest; import gift.service.WishlistService; import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/main/java/gift/domain/Product.java b/src/main/java/gift/domain/Product.java index a38ea489d..5a46a635b 100644 --- a/src/main/java/gift/domain/Product.java +++ b/src/main/java/gift/domain/Product.java @@ -1,7 +1,7 @@ package gift.domain; -import gift.controller.ProductRequest; +import gift.dto.request.ProductRequest; public class Product { private long id; diff --git a/src/main/java/gift/controller/MemberRequest.java b/src/main/java/gift/dto/request/MemberRequest.java similarity index 95% rename from src/main/java/gift/controller/MemberRequest.java rename to src/main/java/gift/dto/request/MemberRequest.java index 788ef225c..69b8b63f3 100644 --- a/src/main/java/gift/controller/MemberRequest.java +++ b/src/main/java/gift/dto/request/MemberRequest.java @@ -1,4 +1,4 @@ -package gift.controller; +package gift.dto.request; import jakarta.validation.constraints.*; diff --git a/src/main/java/gift/controller/ProductRequest.java b/src/main/java/gift/dto/request/ProductRequest.java similarity index 98% rename from src/main/java/gift/controller/ProductRequest.java rename to src/main/java/gift/dto/request/ProductRequest.java index 1b8c66933..35f9de0ea 100644 --- a/src/main/java/gift/controller/ProductRequest.java +++ b/src/main/java/gift/dto/request/ProductRequest.java @@ -1,4 +1,4 @@ -package gift.controller; +package gift.dto.request; import gift.domain.Product; import gift.exception.InvalidProductDataException; diff --git a/src/main/java/gift/controller/WishlistIdRequest.java b/src/main/java/gift/dto/request/WishlistIdRequest.java similarity index 95% rename from src/main/java/gift/controller/WishlistIdRequest.java rename to src/main/java/gift/dto/request/WishlistIdRequest.java index 8a7f0361f..82eb9a80f 100644 --- a/src/main/java/gift/controller/WishlistIdRequest.java +++ b/src/main/java/gift/dto/request/WishlistIdRequest.java @@ -1,4 +1,4 @@ -package gift.controller; +package gift.dto.request; import jakarta.validation.constraints.*; public class WishlistIdRequest { diff --git a/src/main/java/gift/controller/WishlistNameRequest.java b/src/main/java/gift/dto/request/WishlistNameRequest.java similarity index 95% rename from src/main/java/gift/controller/WishlistNameRequest.java rename to src/main/java/gift/dto/request/WishlistNameRequest.java index 641cb7296..ecbc2d8d3 100644 --- a/src/main/java/gift/controller/WishlistNameRequest.java +++ b/src/main/java/gift/dto/request/WishlistNameRequest.java @@ -1,4 +1,4 @@ -package gift.controller; +package gift.dto.request; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/gift/exception/ErrorResponse.java b/src/main/java/gift/dto/response/ErrorResponse.java similarity index 95% rename from src/main/java/gift/exception/ErrorResponse.java rename to src/main/java/gift/dto/response/ErrorResponse.java index ded1e4e38..f2a6c2f36 100644 --- a/src/main/java/gift/exception/ErrorResponse.java +++ b/src/main/java/gift/dto/response/ErrorResponse.java @@ -1,4 +1,4 @@ -package gift.exception; +package gift.dto.response; import java.util.Map; diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java index 4537d91aa..1b51f61e7 100644 --- a/src/main/java/gift/exception/GlobalExceptionHandler.java +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package gift.exception; +import gift.dto.response.ErrorResponse; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/gift/repository/ProductDBRepository.java b/src/main/java/gift/repository/ProductDBRepository.java index 78ae899ab..b52fef52f 100644 --- a/src/main/java/gift/repository/ProductDBRepository.java +++ b/src/main/java/gift/repository/ProductDBRepository.java @@ -1,10 +1,9 @@ package gift.repository; -import gift.controller.ProductRequest; +import gift.dto.request.ProductRequest; import gift.domain.Product; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.core.simple.SimpleJdbcInsert; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; diff --git a/src/main/java/gift/repository/ProductMemoryRepository.java b/src/main/java/gift/repository/ProductMemoryRepository.java index c814fe823..4a14ea3ea 100644 --- a/src/main/java/gift/repository/ProductMemoryRepository.java +++ b/src/main/java/gift/repository/ProductMemoryRepository.java @@ -1,8 +1,7 @@ package gift.repository; -import gift.controller.ProductRequest; +import gift.dto.request.ProductRequest; import gift.domain.Product; -import org.springframework.stereotype.Repository; import java.util.*; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/gift/repository/ProductRepository.java b/src/main/java/gift/repository/ProductRepository.java index 5c6a26f3a..1c5280e60 100644 --- a/src/main/java/gift/repository/ProductRepository.java +++ b/src/main/java/gift/repository/ProductRepository.java @@ -1,6 +1,6 @@ package gift.repository; -import gift.controller.ProductRequest; +import gift.dto.request.ProductRequest; import gift.domain.Product; import java.util.List; diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java index 59908fea4..27e76be83 100644 --- a/src/main/java/gift/service/MemberService.java +++ b/src/main/java/gift/service/MemberService.java @@ -1,19 +1,17 @@ package gift.service; -import gift.controller.MemberRequest; +import gift.dto.request.MemberRequest; import gift.domain.Member; import gift.exception.AccessDeniedException; import gift.exception.InvalidCredentialsException; import gift.exception.MemberNotFoundException; import gift.repository.MemberJDBCRepository; -import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.crypto.SecretKey; -import java.util.Base64; import java.util.Optional; @Service diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java index bb7cf08ae..b3598a820 100644 --- a/src/main/java/gift/service/ProductService.java +++ b/src/main/java/gift/service/ProductService.java @@ -1,21 +1,16 @@ package gift.service; -import gift.controller.ProductRequest; +import gift.dto.request.ProductRequest; import gift.domain.Product; import gift.exception.InvalidProductDataException; import gift.exception.ProductNotFoundException; import gift.repository.ProductRepository; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.NoSuchElementException; -import java.util.Optional; @Service @Transactional() diff --git a/src/main/java/gift/service/WishlistService.java b/src/main/java/gift/service/WishlistService.java index adbdc6be6..c047942c5 100644 --- a/src/main/java/gift/service/WishlistService.java +++ b/src/main/java/gift/service/WishlistService.java @@ -1,7 +1,7 @@ package gift.service; -import gift.controller.WishlistIdRequest; -import gift.controller.WishlistNameRequest; +import gift.dto.request.WishlistIdRequest; +import gift.dto.request.WishlistNameRequest; import gift.domain.WishlistItem; import gift.exception.AccessDeniedException; import gift.exception.MemberNotFoundException; diff --git a/src/test/java/gift/controller/ProductControllerTest.java b/src/test/java/gift/controller/ProductControllerTest.java index c69b77fd9..8fd9a2ec5 100644 --- a/src/test/java/gift/controller/ProductControllerTest.java +++ b/src/test/java/gift/controller/ProductControllerTest.java @@ -1,12 +1,10 @@ package gift.controller; -import gift.controller.ProductController; -import gift.controller.ProductRequest; +import gift.dto.request.ProductRequest; import gift.domain.Product; import gift.service.ProductService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; @@ -17,7 +15,6 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import java.util.Arrays; -import java.util.Optional; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; diff --git a/src/test/java/gift/repository/ProductDBRepositoryTest.java b/src/test/java/gift/repository/ProductDBRepositoryTest.java index 96e39b340..35cc21df5 100644 --- a/src/test/java/gift/repository/ProductDBRepositoryTest.java +++ b/src/test/java/gift/repository/ProductDBRepositoryTest.java @@ -1,8 +1,7 @@ package gift.repository; -import gift.controller.ProductRequest; +import gift.dto.request.ProductRequest; import gift.domain.Product; -import gift.repository.ProductDBRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; diff --git a/src/test/java/gift/repository/ProductMemoryRepositoryTest.java b/src/test/java/gift/repository/ProductMemoryRepositoryTest.java index dfec2ff51..13b2c564d 100644 --- a/src/test/java/gift/repository/ProductMemoryRepositoryTest.java +++ b/src/test/java/gift/repository/ProductMemoryRepositoryTest.java @@ -1,6 +1,6 @@ package gift.repository; -import gift.controller.ProductRequest; +import gift.dto.request.ProductRequest; import gift.domain.Product; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/src/test/java/gift/service/ProductServiceTest.java b/src/test/java/gift/service/ProductServiceTest.java index 379e6ebb1..68f98bf0a 100644 --- a/src/test/java/gift/service/ProductServiceTest.java +++ b/src/test/java/gift/service/ProductServiceTest.java @@ -1,9 +1,8 @@ package gift.service; -import gift.controller.ProductRequest; +import gift.dto.request.ProductRequest; import gift.domain.Product; import gift.repository.ProductRepository; -import gift.service.ProductService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; From 61825acef1b22bb7b0a06fc75c01a7145ba4f118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:59:01 +0900 Subject: [PATCH 41/49] =?UTF-8?q?refactor:=20kakao=ED=8F=AC=ED=95=A8=20?= =?UTF-8?q?=EB=AC=B8=EA=B5=AC=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gift/dto/request/ProductRequest.java | 5 ++-- .../java/gift/validation/KakaoApproval.java | 21 +++++++++++++++++ .../validation/KakaoApprovalValidator.java | 23 +++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/main/java/gift/validation/KakaoApproval.java create mode 100644 src/main/java/gift/validation/KakaoApprovalValidator.java diff --git a/src/main/java/gift/dto/request/ProductRequest.java b/src/main/java/gift/dto/request/ProductRequest.java index 35f9de0ea..dd67a26d9 100644 --- a/src/main/java/gift/dto/request/ProductRequest.java +++ b/src/main/java/gift/dto/request/ProductRequest.java @@ -2,6 +2,7 @@ import gift.domain.Product; import gift.exception.InvalidProductDataException; +import gift.validation.KakaoApproval; import jakarta.validation.constraints.*; @@ -13,6 +14,7 @@ public class ProductRequest { regexp = "^[a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ()\\[\\]+\\-&/_ ]*$", message = "상품 이름에 허용되지 않는 특수 문자가 포함되어 있습니다." ) + @KakaoApproval private String name; @NotNull(message = "가격을 입력하세요") @@ -22,9 +24,6 @@ public class ProductRequest { private String imageUrl; public ProductRequest(String name, long price, String imageUrl) { - if (name.contains("카카오") && !isApprovedByMD()) { - throw new InvalidProductDataException("상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."); - } this.name = name; this.price = price; this.imageUrl = imageUrl; diff --git a/src/main/java/gift/validation/KakaoApproval.java b/src/main/java/gift/validation/KakaoApproval.java new file mode 100644 index 000000000..2930c38b7 --- /dev/null +++ b/src/main/java/gift/validation/KakaoApproval.java @@ -0,0 +1,21 @@ +package gift.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy = KakaoApprovalValidator.class) +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface KakaoApproval { + + String message() default "상품 이름에 '카카오'를 포함할 수 없습니다. 담당 MD와 협의하세요."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/gift/validation/KakaoApprovalValidator.java b/src/main/java/gift/validation/KakaoApprovalValidator.java new file mode 100644 index 000000000..39a917db0 --- /dev/null +++ b/src/main/java/gift/validation/KakaoApprovalValidator.java @@ -0,0 +1,23 @@ +package gift.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class KakaoApprovalValidator implements ConstraintValidator { + + @Override + public void initialize(KakaoApproval constraintAnnotation) { + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + return !value.contains("카카오") || isApprovedByMD(); + } + + private boolean isApprovedByMD() { + return false; + } +} From 5329fb19944bcb836639ab7c57ca8c8024dea55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Mon, 8 Jul 2024 19:11:42 +0900 Subject: [PATCH 42/49] =?UTF-8?q?docs:=20schema.sql=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 9d4741cd3..8864288e4 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -6,7 +6,7 @@ CREATE TABLE member ( ); CREATE TABLE product ( id BIGINT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL UNIQUE , price BIGINT NOT NULL, imageUrl VARCHAR(255) ); From 87f5010554a9fb05bb1ab569749b470aca8fdb75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Mon, 8 Jul 2024 19:19:48 +0900 Subject: [PATCH 43/49] =?UTF-8?q?refactor:=20secretKey=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/domain/Member.java | 17 +---------------- .../gift/repository/MemberJDBCRepository.java | 7 +++---- src/main/java/gift/service/MemberService.java | 4 ++-- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/main/java/gift/domain/Member.java b/src/main/java/gift/domain/Member.java index 2e15db7d4..b4532c1f7 100644 --- a/src/main/java/gift/domain/Member.java +++ b/src/main/java/gift/domain/Member.java @@ -10,21 +10,18 @@ public class Member { private Long id; private String email; private String password; - private String secretKey; public Member() {} - public Member(Long id, String email, String password, String secretKey) { + public Member(Long id, String email, String password) { this.id = id; this.email = email; this.password = password; - this.secretKey = secretKey; } public Member(String email, String password) { this.email = email; this.password = password; - this.secretKey = generateSecretKey(); } public Long getId() { @@ -51,17 +48,5 @@ public void setPassword(String password) { this.password = password; } - public String getSecretKey() { - return secretKey; - } - - public void setSecretKey(String secretKey) { - this.secretKey = secretKey; - } - - private String generateSecretKey() { - return UUID.randomUUID().toString(); - } - } diff --git a/src/main/java/gift/repository/MemberJDBCRepository.java b/src/main/java/gift/repository/MemberJDBCRepository.java index fa51403db..187bac817 100644 --- a/src/main/java/gift/repository/MemberJDBCRepository.java +++ b/src/main/java/gift/repository/MemberJDBCRepository.java @@ -21,8 +21,8 @@ public MemberJDBCRepository(DataSource dataSource) { @Override public Member save(Member member) { - String sql = "INSERT INTO member (email, password, secret_key) VALUES (?, ?, ?)"; - jdbcTemplate.update(sql, member.getEmail(), member.getPassword(), member.getSecretKey()); + String sql = "INSERT INTO member (email, password) VALUES (?, ?, ?)"; + jdbcTemplate.update(sql, member.getEmail(), member.getPassword()); Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class); member.setId(id); return member; @@ -46,8 +46,7 @@ private RowMapper memberRowMapper() { return (rs, rowNum) -> new Member( rs.getLong("id"), rs.getString("email"), - rs.getString("password"), - rs.getString("secret_key") + rs.getString("password") ); } } diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java index 27e76be83..cbc6fb2f6 100644 --- a/src/main/java/gift/service/MemberService.java +++ b/src/main/java/gift/service/MemberService.java @@ -45,10 +45,10 @@ public String authenticate(MemberRequest memberRequest) { } private String generateJwtToken(Member member) { - SecretKey secretKey = Keys.hmacShaKeyFor(member.getSecretKey().getBytes()); + String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; return Jwts.builder() .setSubject(member.getEmail()) - .signWith(secretKey) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) .compact(); } } From 748dfb921df154eb0a47898ed211f929f0cdacb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Mon, 8 Jul 2024 19:29:46 +0900 Subject: [PATCH 44/49] =?UTF-8?q?refactor:=20generateJwtToken=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/service/MemberService.java | 3 ++- src/main/resources/schema.sql | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java index cbc6fb2f6..c61ca425b 100644 --- a/src/main/java/gift/service/MemberService.java +++ b/src/main/java/gift/service/MemberService.java @@ -47,7 +47,8 @@ public String authenticate(MemberRequest memberRequest) { private String generateJwtToken(Member member) { String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; return Jwts.builder() - .setSubject(member.getEmail()) + .setSubject(member.getId().toString()) + .claim("email", member.getEmail()) .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) .compact(); } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 8864288e4..6abda061d 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -2,7 +2,6 @@ CREATE TABLE member ( id BIGINT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, - secretKey VARCHAR(255) NOT NULL ); CREATE TABLE product ( id BIGINT AUTO_INCREMENT PRIMARY KEY, From 73410f6f74d61f54ff350f380f12dab65f7de65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Tue, 9 Jul 2024 01:37:34 +0900 Subject: [PATCH 45/49] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gift/config/FilterConfiguration.java | 40 +++++++++++ .../java/gift/controller/HomeController.java | 14 ++++ .../gift/controller/MemberController.java | 45 ++++++------- src/main/java/gift/domain/TokenAuth.java | 10 +++ .../exception/GlobalExceptionHandler.java | 5 ++ .../exception/UnAuthorizationException.java | 7 ++ src/main/java/gift/filter/AuthFilter.java | 67 +++++++++++++++++++ src/main/java/gift/filter/LoginFilter.java | 53 +++++++++++++++ .../gift/repository/TokenJDBCRepository.java | 44 ++++++++++++ .../java/gift/repository/TokenRepository.java | 12 ++++ src/main/java/gift/service/MemberService.java | 20 ++---- src/main/java/gift/service/TokenService.java | 41 ++++++++++++ src/main/resources/schema.sql | 10 ++- 13 files changed, 327 insertions(+), 41 deletions(-) create mode 100644 src/main/java/gift/config/FilterConfiguration.java create mode 100644 src/main/java/gift/controller/HomeController.java create mode 100644 src/main/java/gift/domain/TokenAuth.java create mode 100644 src/main/java/gift/exception/UnAuthorizationException.java create mode 100644 src/main/java/gift/filter/AuthFilter.java create mode 100644 src/main/java/gift/filter/LoginFilter.java create mode 100644 src/main/java/gift/repository/TokenJDBCRepository.java create mode 100644 src/main/java/gift/repository/TokenRepository.java create mode 100644 src/main/java/gift/service/TokenService.java diff --git a/src/main/java/gift/config/FilterConfiguration.java b/src/main/java/gift/config/FilterConfiguration.java new file mode 100644 index 000000000..65653efd7 --- /dev/null +++ b/src/main/java/gift/config/FilterConfiguration.java @@ -0,0 +1,40 @@ +package gift.config; + +import gift.repository.TokenRepository; +import gift.filter.AuthFilter; +import gift.filter.LoginFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import jakarta.servlet.Filter; + +@Configuration +public class FilterConfiguration { + + private final TokenRepository tokenRepository; + + @Autowired + public FilterConfiguration(TokenRepository tokenRepository) { + this.tokenRepository = tokenRepository; + } + + @Bean(name = "authFilterBean") + public FilterRegistrationBean authFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new AuthFilter(tokenRepository)); + filterRegistrationBean.setOrder(1); + filterRegistrationBean.addUrlPatterns("/*"); + return filterRegistrationBean; + } + + @Bean(name = "loginFilterBean") + public FilterRegistrationBean loginFilter() { + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(); + filterRegistrationBean.setFilter(new LoginFilter(tokenRepository)); + filterRegistrationBean.setOrder(2); + filterRegistrationBean.addUrlPatterns("/members/login"); + return filterRegistrationBean; + } +} diff --git a/src/main/java/gift/controller/HomeController.java b/src/main/java/gift/controller/HomeController.java new file mode 100644 index 000000000..27f3b4252 --- /dev/null +++ b/src/main/java/gift/controller/HomeController.java @@ -0,0 +1,14 @@ +package gift.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HomeController { + + @GetMapping("/home") + public String home(){ + return "HomePage"; + } + +} \ No newline at end of file diff --git a/src/main/java/gift/controller/MemberController.java b/src/main/java/gift/controller/MemberController.java index 13666ad07..c3bfe9225 100644 --- a/src/main/java/gift/controller/MemberController.java +++ b/src/main/java/gift/controller/MemberController.java @@ -1,48 +1,45 @@ package gift.controller; import gift.dto.request.MemberRequest; +import gift.domain.Member; import gift.service.MemberService; +import gift.service.TokenService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.util.HashMap; +import java.util.Map; + @RestController +@RequestMapping("/members") public class MemberController { private final MemberService memberService; + private final TokenService tokenService; @Autowired - public MemberController(MemberService memberService) { + public MemberController(MemberService memberService, TokenService tokenService) { this.memberService = memberService; + this.tokenService = tokenService; } - @PostMapping("/members/register") - public ResponseEntity register(@RequestBody MemberRequest memberRequest) { - String token = memberService.register(memberRequest); - TokenResponse response = new TokenResponse(token); + @PostMapping("/register") + public ResponseEntity> register(@RequestBody MemberRequest memberRequest) { + Member member = memberService.register(memberRequest); + String token = tokenService.saveToken(member); + Map response = new HashMap<>(); + response.put("token", token); return ResponseEntity.ok(response); } - @PostMapping("/members/login") - public ResponseEntity login(@RequestBody MemberRequest memberRequest) { - String token = memberService.authenticate(memberRequest); - TokenResponse response = new TokenResponse(token); + @PostMapping("/login") + public ResponseEntity> login(@RequestBody MemberRequest memberRequest) { + Member member = memberService.authenticate(memberRequest); + String token = tokenService.saveToken(member); + Map response = new HashMap<>(); + response.put("token", token); return ResponseEntity.ok(response); } - private static class TokenResponse { - private String token; - - public TokenResponse(String token) { - this.token = token; - } - - public String getToken() { - return token; - } - - public void setToken(String token) { - this.token = token; - } - } } diff --git a/src/main/java/gift/domain/TokenAuth.java b/src/main/java/gift/domain/TokenAuth.java new file mode 100644 index 000000000..625915957 --- /dev/null +++ b/src/main/java/gift/domain/TokenAuth.java @@ -0,0 +1,10 @@ +package gift.domain; + +public class TokenAuth { + private String token; + private String email; + public TokenAuth(String token, String email){ + this.token = token; + this.email = email; + } +} diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java index 1b51f61e7..0365b2e6b 100644 --- a/src/main/java/gift/exception/GlobalExceptionHandler.java +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -62,6 +62,11 @@ public ResponseEntity handleAccessDeniedException(AccessDeniedExc return buildErrorResponse("접근이 거부되었습니다: " + ex.getMessage(), "403", HttpStatus.FORBIDDEN); } + @ExceptionHandler(UnAuthorizationException.class) + public ResponseEntity handleUnAuthorizationException(UnAuthorizationException ex){ + return buildErrorResponse("로그인 실패: " + ex.getMessage(), "401", HttpStatus.UNAUTHORIZED); + } + private ResponseEntity buildErrorResponse(String message, String status, HttpStatus httpStatus) { ErrorResponse errorResponse = new ErrorResponse(message, status); return new ResponseEntity<>(errorResponse, httpStatus); diff --git a/src/main/java/gift/exception/UnAuthorizationException.java b/src/main/java/gift/exception/UnAuthorizationException.java new file mode 100644 index 000000000..9d5c60e9f --- /dev/null +++ b/src/main/java/gift/exception/UnAuthorizationException.java @@ -0,0 +1,7 @@ +package gift.exception; + +public class UnAuthorizationException extends RuntimeException { + public UnAuthorizationException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/gift/filter/AuthFilter.java b/src/main/java/gift/filter/AuthFilter.java new file mode 100644 index 000000000..d2853a251 --- /dev/null +++ b/src/main/java/gift/filter/AuthFilter.java @@ -0,0 +1,67 @@ +package gift.filter; + +import gift.domain.TokenAuth; +import gift.repository.TokenRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + + +public class AuthFilter implements Filter { + + private final TokenRepository tokenRepository; + + public AuthFilter(TokenRepository tokenRepository) { + this.tokenRepository = tokenRepository; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + String path = httpRequest.getRequestURI(); + + if (path.equals("/home") || path.startsWith("/members") || path.startsWith("/h2-console")) { + filterChain.doFilter(request, response); + return; + } + + String authHeader = httpRequest.getHeader("Authorization"); + + if (authHeader == null || authHeader.isEmpty()){ + httpResponse.sendRedirect("/home"); + return; + } + + String token = authHeader.substring(7); + + Optional tokenAuthOptional = tokenRepository.findTokenByToken(token); + + if (tokenAuthOptional.isEmpty()){ + httpResponse.sendRedirect("/home"); + return; + } + + filterChain.doFilter(request, response); + } + + @Override + public void destroy() { + } +} diff --git a/src/main/java/gift/filter/LoginFilter.java b/src/main/java/gift/filter/LoginFilter.java new file mode 100644 index 000000000..eefcd79b7 --- /dev/null +++ b/src/main/java/gift/filter/LoginFilter.java @@ -0,0 +1,53 @@ +package gift.filter; + + + +import gift.domain.TokenAuth; +import gift.repository.TokenRepository; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.Optional; + +public class LoginFilter implements Filter { + + private final TokenRepository tokenRepository; + + public LoginFilter(TokenRepository tokenRepository) { + this.tokenRepository = tokenRepository; + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + String authHeader = httpRequest.getHeader("Authorization"); + + if (!(authHeader == null || authHeader.isEmpty())){ + String token = authHeader.substring(7); + Optional tokenAuthOptional = tokenRepository.findTokenByToken(token); + + if (tokenAuthOptional.isEmpty()){ + filterChain.doFilter(request, response); + return; + } + + httpResponse.sendRedirect("/home"); + return; + } + + filterChain.doFilter(request, response); + } + + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/src/main/java/gift/repository/TokenJDBCRepository.java b/src/main/java/gift/repository/TokenJDBCRepository.java new file mode 100644 index 000000000..94dc9f267 --- /dev/null +++ b/src/main/java/gift/repository/TokenJDBCRepository.java @@ -0,0 +1,44 @@ +package gift.repository; + + +import gift.domain.TokenAuth; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import javax.sql.DataSource; +import java.util.List; +import java.util.Optional; + +@Repository + +public class TokenJDBCRepository implements TokenRepository { + + private final JdbcTemplate jdbcTemplate; + + public TokenJDBCRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public String save(String token, String email) { + String sql = "INSERT INTO tokenauth(token, email) VALUES (?, ?)"; + jdbcTemplate.update(sql, token, email); + return token; + } + + public Optional findTokenByToken(String token){ + String sql = "SELECT token, email FROM tokenauth WHERE token = ?"; + List tokenAuths = jdbcTemplate.query(sql, new Object[]{token}, (rs, rowNum) -> new TokenAuth( + rs.getString("token"), + rs.getString("email") + )); + + if (tokenAuths.isEmpty()){ + return Optional.empty(); + } + + return Optional.of(tokenAuths.getFirst()); + } + +} + diff --git a/src/main/java/gift/repository/TokenRepository.java b/src/main/java/gift/repository/TokenRepository.java new file mode 100644 index 000000000..21d9e74e6 --- /dev/null +++ b/src/main/java/gift/repository/TokenRepository.java @@ -0,0 +1,12 @@ +package gift.repository; + + +import gift.domain.TokenAuth; + +import java.util.Optional; + +public interface TokenRepository { + String save(String token, String email); + + Optional findTokenByToken(String substring); +} diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java index c61ca425b..b54bc4218 100644 --- a/src/main/java/gift/service/MemberService.java +++ b/src/main/java/gift/service/MemberService.java @@ -6,12 +6,9 @@ import gift.exception.InvalidCredentialsException; import gift.exception.MemberNotFoundException; import gift.repository.MemberJDBCRepository; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import javax.crypto.SecretKey; import java.util.Optional; @Service @@ -24,32 +21,23 @@ public MemberService(MemberJDBCRepository memberRepository) { this.memberRepository = memberRepository; } - public String register(MemberRequest memberRequest) { + public Member register(MemberRequest memberRequest) { Optional oldMember = memberRepository.findByEmail(memberRequest.getEmail()); if (oldMember.isPresent()) { throw new AccessDeniedException("이미 등록된 이메일입니다."); } Member member = new Member(memberRequest.getEmail(), memberRequest.getPassword()); memberRepository.save(member); - return generateJwtToken(member); + return member; } - public String authenticate(MemberRequest memberRequest) { + public Member authenticate(MemberRequest memberRequest) { Member member = memberRepository.findByEmail(memberRequest.getEmail()) .orElseThrow(() -> new MemberNotFoundException("존재하지 않는 회원입니다.")); if (!memberRequest.getPassword().equals(member.getPassword())) { throw new InvalidCredentialsException("잘못된 비밀번호입니다."); } - return generateJwtToken(member); - } - - private String generateJwtToken(Member member) { - String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; - return Jwts.builder() - .setSubject(member.getId().toString()) - .claim("email", member.getEmail()) - .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) - .compact(); + return member; } } diff --git a/src/main/java/gift/service/TokenService.java b/src/main/java/gift/service/TokenService.java new file mode 100644 index 000000000..f5f70a659 --- /dev/null +++ b/src/main/java/gift/service/TokenService.java @@ -0,0 +1,41 @@ +package gift.service; + +import gift.domain.Member; +import gift.domain.TokenAuth; +import gift.exception.UnAuthorizationException; +import gift.repository.TokenRepository; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Service; + +import java.nio.charset.StandardCharsets; +import java.util.Date; + +@Service +public class TokenService { + + private final TokenRepository tokenRepository; + + public TokenService(TokenRepository tokenRepository) { + this.tokenRepository = tokenRepository; + } + + public String saveToken(Member member){ + String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; + String accessToken = Jwts.builder() + .setSubject(member.getId().toString()) + .claim("email", member.getEmail()) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) + .compact(); + return tokenRepository.save(accessToken, member.getEmail()); + } + + public TokenAuth findToken(String token){ + TokenAuth tokenAuth = tokenRepository.findTokenByToken(token) + .orElseThrow(()-> new UnAuthorizationException("인증되지 않은 사용자입니다. 다시 로그인 해주세요.")); + + return tokenAuth; + } + +} diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 6abda061d..67986083f 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -1,7 +1,7 @@ CREATE TABLE member ( id BIGINT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, - password VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL ); CREATE TABLE product ( id BIGINT AUTO_INCREMENT PRIMARY KEY, @@ -18,3 +18,11 @@ CREATE TABLE wishlist ( REFERENCES member(id) ON DELETE CASCADE ); +CREATE TABLE tokenauth ( + token VARCHAR(255) NOT NULL PRIMARY KEY, + email VARCHAR(255) NOT NULL, + CONSTRAINT fk_member_email + FOREIGN KEY (email) + REFERENCES member(email) + ON DELETE CASCADE +); From 889881ddcc75cac35a05f6c7794772248876d922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Tue, 9 Jul 2024 01:49:52 +0900 Subject: [PATCH 46/49] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=A9=94=EC=9D=BC?= =?UTF-8?q?=20=EC=A4=91=EB=B3=B5=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/exception/AccessDeniedException.java | 8 -------- .../java/gift/exception/DuplicateMemberException.java | 7 +++++++ src/main/java/gift/exception/GlobalExceptionHandler.java | 6 +++--- src/main/java/gift/service/MemberService.java | 4 ++-- 4 files changed, 12 insertions(+), 13 deletions(-) delete mode 100644 src/main/java/gift/exception/AccessDeniedException.java create mode 100644 src/main/java/gift/exception/DuplicateMemberException.java diff --git a/src/main/java/gift/exception/AccessDeniedException.java b/src/main/java/gift/exception/AccessDeniedException.java deleted file mode 100644 index e5a83c679..000000000 --- a/src/main/java/gift/exception/AccessDeniedException.java +++ /dev/null @@ -1,8 +0,0 @@ -package gift.exception; - -public class AccessDeniedException extends RuntimeException { - - public AccessDeniedException(String message) { - super(message); - } -} diff --git a/src/main/java/gift/exception/DuplicateMemberException.java b/src/main/java/gift/exception/DuplicateMemberException.java new file mode 100644 index 000000000..835d91d2a --- /dev/null +++ b/src/main/java/gift/exception/DuplicateMemberException.java @@ -0,0 +1,7 @@ +package gift.exception; + +public class DuplicateMemberException extends RuntimeException { + public DuplicateMemberException(String message) { + super(message); + } +} diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java index 0365b2e6b..b8bab39bd 100644 --- a/src/main/java/gift/exception/GlobalExceptionHandler.java +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -57,9 +57,9 @@ public ResponseEntity handleInvalidCredentialsException(InvalidCr return buildErrorResponse("로그인 실패: " + ex.getMessage(), "401", HttpStatus.UNAUTHORIZED); } - @ExceptionHandler(AccessDeniedException.class) - public ResponseEntity handleAccessDeniedException(AccessDeniedException ex) { - return buildErrorResponse("접근이 거부되었습니다: " + ex.getMessage(), "403", HttpStatus.FORBIDDEN); + @ExceptionHandler(DuplicateMemberException.class) // 409 Conflict + public ResponseEntity handleDuplicateMemberException(DuplicateMemberException ex) { + return buildErrorResponse("로그인 실패: " + ex.getMessage(), "409", HttpStatus.CONFLICT); } @ExceptionHandler(UnAuthorizationException.class) diff --git a/src/main/java/gift/service/MemberService.java b/src/main/java/gift/service/MemberService.java index b54bc4218..63afeca3a 100644 --- a/src/main/java/gift/service/MemberService.java +++ b/src/main/java/gift/service/MemberService.java @@ -2,7 +2,7 @@ import gift.dto.request.MemberRequest; import gift.domain.Member; -import gift.exception.AccessDeniedException; +import gift.exception.DuplicateMemberException; import gift.exception.InvalidCredentialsException; import gift.exception.MemberNotFoundException; import gift.repository.MemberJDBCRepository; @@ -24,7 +24,7 @@ public MemberService(MemberJDBCRepository memberRepository) { public Member register(MemberRequest memberRequest) { Optional oldMember = memberRepository.findByEmail(memberRequest.getEmail()); if (oldMember.isPresent()) { - throw new AccessDeniedException("이미 등록된 이메일입니다."); + throw new DuplicateMemberException("이미 등록된 이메일입니다."); } Member member = new Member(memberRequest.getEmail(), memberRequest.getPassword()); memberRepository.save(member); From 8eaf29a4af081fd808dc77b35e54582d25185ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Tue, 9 Jul 2024 02:08:29 +0900 Subject: [PATCH 47/49] =?UTF-8?q?refactor:=20=EC=9C=84=EC=8B=9C=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC,=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gift/controller/WishlistController.java | 13 ++++++------- .../gift/exception/AccessDeniedException.java | 7 +++++++ .../gift/exception/GlobalExceptionHandler.java | 5 +++++ src/main/java/gift/service/WishlistService.java | 16 ++++++---------- 4 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 src/main/java/gift/exception/AccessDeniedException.java diff --git a/src/main/java/gift/controller/WishlistController.java b/src/main/java/gift/controller/WishlistController.java index 0356dafbb..7039901eb 100644 --- a/src/main/java/gift/controller/WishlistController.java +++ b/src/main/java/gift/controller/WishlistController.java @@ -1,7 +1,6 @@ package gift.controller; import gift.domain.WishlistItem; -import gift.dto.request.WishlistIdRequest; import gift.dto.request.WishlistNameRequest; import gift.service.WishlistService; import jakarta.validation.Valid; @@ -10,8 +9,8 @@ import java.util.List; - @RestController +@RequestMapping("/wishlist") public class WishlistController { private final WishlistService wishlistService; @@ -21,17 +20,17 @@ public WishlistController(WishlistService wishlistService) { this.wishlistService = wishlistService; } - @PostMapping("/wishlist/add") + @PostMapping public void addToWishlist(@Valid @RequestBody WishlistNameRequest request) { wishlistService.addItemToWishlist(request); } - @DeleteMapping("/wishlist/delete") - public void deleteFromWishlist(@Valid @RequestBody WishlistIdRequest request) { - wishlistService.deleteItemFromWishlist(request); + @DeleteMapping("/{itemId}") + public void deleteFromWishlist(@PathVariable Long itemId, @RequestParam Long memberId) { + wishlistService.deleteItemFromWishlist(itemId, memberId); } - @GetMapping("/wishlist/get/{memberId}") + @GetMapping("/{memberId}") public List getWishlist(@PathVariable Long memberId) { return wishlistService.getWishlistByMemberId(memberId); } diff --git a/src/main/java/gift/exception/AccessDeniedException.java b/src/main/java/gift/exception/AccessDeniedException.java new file mode 100644 index 000000000..61173a696 --- /dev/null +++ b/src/main/java/gift/exception/AccessDeniedException.java @@ -0,0 +1,7 @@ +package gift.exception; + +public class AccessDeniedException extends RuntimeException{ + public AccessDeniedException(String message) { + super(message); + } +} diff --git a/src/main/java/gift/exception/GlobalExceptionHandler.java b/src/main/java/gift/exception/GlobalExceptionHandler.java index b8bab39bd..be929946a 100644 --- a/src/main/java/gift/exception/GlobalExceptionHandler.java +++ b/src/main/java/gift/exception/GlobalExceptionHandler.java @@ -67,6 +67,11 @@ public ResponseEntity handleUnAuthorizationException(UnAuthorizat return buildErrorResponse("로그인 실패: " + ex.getMessage(), "401", HttpStatus.UNAUTHORIZED); } + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleUnAuthorizationException(AccessDeniedException ex){ + return buildErrorResponse("삭제 실패: " + ex.getMessage(), "403", HttpStatus.FORBIDDEN); + } + private ResponseEntity buildErrorResponse(String message, String status, HttpStatus httpStatus) { ErrorResponse errorResponse = new ErrorResponse(message, status); return new ResponseEntity<>(errorResponse, httpStatus); diff --git a/src/main/java/gift/service/WishlistService.java b/src/main/java/gift/service/WishlistService.java index c047942c5..54e324e7c 100644 --- a/src/main/java/gift/service/WishlistService.java +++ b/src/main/java/gift/service/WishlistService.java @@ -1,9 +1,7 @@ package gift.service; -import gift.dto.request.WishlistIdRequest; import gift.dto.request.WishlistNameRequest; import gift.domain.WishlistItem; -import gift.exception.AccessDeniedException; import gift.exception.MemberNotFoundException; import gift.repository.WishlistRepository; import org.springframework.beans.factory.annotation.Autowired; @@ -26,18 +24,16 @@ public void addItemToWishlist(WishlistNameRequest wishlistNameRequest) { wishlistRepository.addItem(item); } - public void deleteItemFromWishlist(WishlistIdRequest wishlistIdRequest) { - WishlistItem existingItem = wishlistRepository.getItemsByMemberId(wishlistIdRequest.getMemberId()) + public void deleteItemFromWishlist(Long itemId, Long memberId) { + boolean itemExists = wishlistRepository.getItemsByMemberId(memberId) .stream() - .filter(item -> item.getId().equals(wishlistIdRequest.getItemId())) - .findFirst() - .orElseThrow(() -> new MemberNotFoundException("위시리스트가 비어있습니다: " + wishlistIdRequest.getMemberId())); + .anyMatch(item -> item.getId().equals(itemId)); - if (!existingItem.getMemberId().equals(wishlistIdRequest.getMemberId())) { - throw new AccessDeniedException("아이템 삭제 권한이 없습니다."); + if (!itemExists) { + throw new MemberNotFoundException("해당 아이템이 존재하지 않습니다: " + itemId); } - wishlistRepository.deleteItem(wishlistIdRequest.getItemId()); + wishlistRepository.deleteItem(itemId); } public List getWishlistByMemberId(Long memberId) { From 147d1addfc05f537d055ec949a567099de4c67b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Tue, 9 Jul 2024 03:43:55 +0900 Subject: [PATCH 48/49] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gift/controller/WishlistController.java | 10 +++--- src/main/java/gift/domain/TokenAuth.java | 12 +++++-- src/main/java/gift/filter/AuthFilter.java | 33 ++++++++++--------- src/main/java/gift/filter/LoginFilter.java | 21 ++++++------ .../gift/repository/TokenJDBCRepository.java | 15 ++++----- src/main/java/gift/service/TokenService.java | 33 ++++++++++++++++--- .../java/gift/service/WishlistService.java | 14 +++++--- 7 files changed, 87 insertions(+), 51 deletions(-) diff --git a/src/main/java/gift/controller/WishlistController.java b/src/main/java/gift/controller/WishlistController.java index 7039901eb..d3da7ff66 100644 --- a/src/main/java/gift/controller/WishlistController.java +++ b/src/main/java/gift/controller/WishlistController.java @@ -21,16 +21,16 @@ public WishlistController(WishlistService wishlistService) { } @PostMapping - public void addToWishlist(@Valid @RequestBody WishlistNameRequest request) { - wishlistService.addItemToWishlist(request); + public void addToWishlist(@Valid @RequestBody WishlistNameRequest request, @RequestHeader("Authorization") String token) { + wishlistService.addItemToWishlist(request, token); } @DeleteMapping("/{itemId}") - public void deleteFromWishlist(@PathVariable Long itemId, @RequestParam Long memberId) { - wishlistService.deleteItemFromWishlist(itemId, memberId); + public void deleteFromWishlist(@PathVariable Long itemId, @RequestHeader("Authorization") String token) { + wishlistService.deleteItemFromWishlist(itemId, token); } - @GetMapping("/{memberId}") + @GetMapping("/member/{memberId}") public List getWishlist(@PathVariable Long memberId) { return wishlistService.getWishlistByMemberId(memberId); } diff --git a/src/main/java/gift/domain/TokenAuth.java b/src/main/java/gift/domain/TokenAuth.java index 625915957..0fb10b3f7 100644 --- a/src/main/java/gift/domain/TokenAuth.java +++ b/src/main/java/gift/domain/TokenAuth.java @@ -1,10 +1,18 @@ package gift.domain; public class TokenAuth { - private String token; - private String email; + private final String token; + private final String email; public TokenAuth(String token, String email){ this.token = token; this.email = email; } + + public String getToken() { + return token; + } + + public String getEmail() { + return email; + } } diff --git a/src/main/java/gift/filter/AuthFilter.java b/src/main/java/gift/filter/AuthFilter.java index d2853a251..6af0c253a 100644 --- a/src/main/java/gift/filter/AuthFilter.java +++ b/src/main/java/gift/filter/AuthFilter.java @@ -2,26 +2,20 @@ import gift.domain.TokenAuth; import gift.repository.TokenRepository; +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Optional; - +@Component public class AuthFilter implements Filter { private final TokenRepository tokenRepository; + @Autowired public AuthFilter(TokenRepository tokenRepository) { this.tokenRepository = tokenRepository; } @@ -37,23 +31,21 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha String path = httpRequest.getRequestURI(); - if (path.equals("/home") || path.startsWith("/members") || path.startsWith("/h2-console")) { + if (isUnauthenticatedPath(path)) { filterChain.doFilter(request, response); return; } String authHeader = httpRequest.getHeader("Authorization"); - if (authHeader == null || authHeader.isEmpty()){ + if (authHeader == null || !authHeader.startsWith("Bearer ")) { httpResponse.sendRedirect("/home"); return; } String token = authHeader.substring(7); - Optional tokenAuthOptional = tokenRepository.findTokenByToken(token); - - if (tokenAuthOptional.isEmpty()){ + if (!isTokenValid(token)) { httpResponse.sendRedirect("/home"); return; } @@ -64,4 +56,13 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha @Override public void destroy() { } + + private boolean isUnauthenticatedPath(String path) { + return path.equals("/home") || path.startsWith("/members") || path.startsWith("/h2-console"); + } + + private boolean isTokenValid(String token) { + TokenAuth tokenAuth = tokenRepository.findTokenByToken(token).orElse(null); + return tokenAuth != null; + } } diff --git a/src/main/java/gift/filter/LoginFilter.java b/src/main/java/gift/filter/LoginFilter.java index eefcd79b7..17b7c999d 100644 --- a/src/main/java/gift/filter/LoginFilter.java +++ b/src/main/java/gift/filter/LoginFilter.java @@ -1,20 +1,22 @@ package gift.filter; - - import gift.domain.TokenAuth; import gift.repository.TokenRepository; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import java.io.IOException; import java.util.Optional; +@Component public class LoginFilter implements Filter { private final TokenRepository tokenRepository; + @Autowired public LoginFilter(TokenRepository tokenRepository) { this.tokenRepository = tokenRepository; } @@ -30,24 +32,23 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha String authHeader = httpRequest.getHeader("Authorization"); - if (!(authHeader == null || authHeader.isEmpty())){ + if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); Optional tokenAuthOptional = tokenRepository.findTokenByToken(token); - if (tokenAuthOptional.isEmpty()){ + if (tokenAuthOptional.isPresent()) { + TokenAuth tokenAuth = tokenAuthOptional.get(); + // 인증된 사용자라면 다음 필터로 요청을 전달 filterChain.doFilter(request, response); return; } - - httpResponse.sendRedirect("/home"); - return; } - filterChain.doFilter(request, response); + // 인증되지 않은 경우 혹은 토큰이 유효하지 않은 경우 + httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } - @Override public void destroy() { } -} \ No newline at end of file +} diff --git a/src/main/java/gift/repository/TokenJDBCRepository.java b/src/main/java/gift/repository/TokenJDBCRepository.java index 94dc9f267..7aa2a6aa2 100644 --- a/src/main/java/gift/repository/TokenJDBCRepository.java +++ b/src/main/java/gift/repository/TokenJDBCRepository.java @@ -1,7 +1,7 @@ package gift.repository; - import gift.domain.TokenAuth; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @@ -10,35 +10,34 @@ import java.util.Optional; @Repository - public class TokenJDBCRepository implements TokenRepository { private final JdbcTemplate jdbcTemplate; + @Autowired public TokenJDBCRepository(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @Override public String save(String token, String email) { - String sql = "INSERT INTO tokenauth(token, email) VALUES (?, ?)"; + String sql = "INSERT INTO tokenauth (token, email) VALUES (?, ?)"; jdbcTemplate.update(sql, token, email); return token; } - public Optional findTokenByToken(String token){ + @Override + public Optional findTokenByToken(String token) { String sql = "SELECT token, email FROM tokenauth WHERE token = ?"; List tokenAuths = jdbcTemplate.query(sql, new Object[]{token}, (rs, rowNum) -> new TokenAuth( rs.getString("token"), rs.getString("email") )); - if (tokenAuths.isEmpty()){ + if (tokenAuths.isEmpty()) { return Optional.empty(); } - return Optional.of(tokenAuths.getFirst()); + return Optional.of(tokenAuths.get(0)); } - } - diff --git a/src/main/java/gift/service/TokenService.java b/src/main/java/gift/service/TokenService.java index f5f70a659..c711a7ce7 100644 --- a/src/main/java/gift/service/TokenService.java +++ b/src/main/java/gift/service/TokenService.java @@ -5,37 +5,60 @@ import gift.exception.UnAuthorizationException; import gift.repository.TokenRepository; import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import org.springframework.stereotype.Service; +import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; -import java.util.Date; @Service public class TokenService { private final TokenRepository tokenRepository; + private final String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; public TokenService(TokenRepository tokenRepository) { this.tokenRepository = tokenRepository; } public String saveToken(Member member){ - String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; String accessToken = Jwts.builder() .setSubject(member.getId().toString()) .claim("email", member.getEmail()) - .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) + .signWith(getSecretKey()) .compact(); return tokenRepository.save(accessToken, member.getEmail()); } public TokenAuth findToken(String token){ - TokenAuth tokenAuth = tokenRepository.findTokenByToken(token) + return tokenRepository.findTokenByToken(token) .orElseThrow(()-> new UnAuthorizationException("인증되지 않은 사용자입니다. 다시 로그인 해주세요.")); + } + + public String getMemberIdFromToken(String token) { + Claims claims = parseToken(token); + return claims.getSubject(); + } + - return tokenAuth; + public Claims parseToken(String token) { + SecretKey key = getSecretKey(); + JwtParser parser = (JwtParser) Jwts.parser().setSigningKey(key); + return parser.parseClaimsJws(token).getBody(); } + private SecretKey getSecretKey() { + return Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + } + + public boolean validateToken(String token) { + try { + parseToken(token); + return true; + } catch (Exception e) { + return false; + } + } } diff --git a/src/main/java/gift/service/WishlistService.java b/src/main/java/gift/service/WishlistService.java index 54e324e7c..e55695736 100644 --- a/src/main/java/gift/service/WishlistService.java +++ b/src/main/java/gift/service/WishlistService.java @@ -13,19 +13,23 @@ public class WishlistService { private final WishlistRepository wishlistRepository; + private final TokenService tokenService; @Autowired - public WishlistService(WishlistRepository wishlistRepository) { + public WishlistService(WishlistRepository wishlistRepository, TokenService tokenService) { this.wishlistRepository = wishlistRepository; + this.tokenService = tokenService; } - public void addItemToWishlist(WishlistNameRequest wishlistNameRequest) { - WishlistItem item = new WishlistItem(wishlistNameRequest.getMemberId(), wishlistNameRequest.getItemName()); + public void addItemToWishlist(WishlistNameRequest wishlistNameRequest, String token) { + String memberId = tokenService.getMemberIdFromToken(token); + WishlistItem item = new WishlistItem(Long.parseLong(memberId), wishlistNameRequest.getItemName()); wishlistRepository.addItem(item); } - public void deleteItemFromWishlist(Long itemId, Long memberId) { - boolean itemExists = wishlistRepository.getItemsByMemberId(memberId) + public void deleteItemFromWishlist(Long itemId, String token) { + String memberId = tokenService.getMemberIdFromToken(token); + boolean itemExists = wishlistRepository.getItemsByMemberId(Long.parseLong(memberId)) .stream() .anyMatch(item -> item.getId().equals(itemId)); From 7228fad5b9ad27fc5a2b887bdea8c21cba9408fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=A7=84=ED=83=9D?= <87135698+jjt4515@users.noreply.github.com> Date: Tue, 9 Jul 2024 03:49:09 +0900 Subject: [PATCH 49/49] =?UTF-8?q?refactor:=20ProductService=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/gift/service/ProductService.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/gift/service/ProductService.java b/src/main/java/gift/service/ProductService.java index b3598a820..791baae9e 100644 --- a/src/main/java/gift/service/ProductService.java +++ b/src/main/java/gift/service/ProductService.java @@ -41,10 +41,13 @@ public Product findOne(Long productId){ .orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); } - public Product update(Long productId, ProductRequest productRequest){ - return productRepository.updateById(productId, productRequest) - .orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); - + public Product update(Long productId, ProductRequest productRequest) { + try { + return productRepository.updateById(productId, productRequest) + .orElseThrow(() -> new ProductNotFoundException("존재하지 않는 상품입니다.")); + } catch (DataIntegrityViolationException e) { + throw new InvalidProductDataException("상품 데이터가 유효하지 않습니다: " + e.getMessage(), e); + } } public Product delete(Long productId){