diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/api/CategoryController.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/api/CategoryController.java new file mode 100644 index 0000000..01e9195 --- /dev/null +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/api/CategoryController.java @@ -0,0 +1,47 @@ +package com.ssafy.springbootapi.domain.product.api; + +import com.ssafy.springbootapi.domain.product.application.CategoryService; +import com.ssafy.springbootapi.domain.product.domain.Category; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "Category", description = "Category 관련 API 입니다.") +@RestController +@RequiredArgsConstructor +//@CrossOrigin // spring security 디펜던시 삭제로 인한 주석처리 +@RequestMapping("/api/v1/categories") +public class CategoryController { + private final CategoryService categoryService; + + @Operation(summary = "모든 카테고리 조회") + @GetMapping + public ResponseEntity> getAllCategories() { + List categories = categoryService.getAllCategories(); + return ResponseEntity.status(HttpStatus.OK).body(categories); + } + + @Operation(summary = "카테고리 생성") + @PostMapping + public ResponseEntity createCategory(@RequestBody String name) { + Category category = categoryService.createCategory(name); + return ResponseEntity.status(HttpStatus.CREATED).body(category); + } + + @Operation(summary = "카테고리 수정") + @PutMapping("/{id}") + public ResponseEntity updateCategory(@PathVariable Integer id, @RequestBody String name) { + return ResponseEntity.status(HttpStatus.OK).body(categoryService.updateCategory(id, name)); + } + + @Operation(summary = "카테고리 삭제") + @DeleteMapping("/{id}") + public ResponseEntity deleteCategory(@PathVariable Integer id) { + return ResponseEntity.status(HttpStatus.NO_CONTENT).body(categoryService.deleteCategory(id)); + } +} diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/application/CategoryService.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/application/CategoryService.java new file mode 100644 index 0000000..e8dc4d5 --- /dev/null +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/application/CategoryService.java @@ -0,0 +1,85 @@ +package com.ssafy.springbootapi.domain.product.application; + +import com.ssafy.springbootapi.domain.product.dao.CategoryRepository; +import com.ssafy.springbootapi.domain.product.domain.Category; +import com.ssafy.springbootapi.domain.product.exception.NotFoundCategoryException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CategoryService { + private final CategoryRepository categoryRepository; + + /** + * 카테고리 전체 조회 + * @return List(Category) + */ + public List getAllCategories() { + return categoryRepository.findAll(); + } + + /** + * id로 카테고리 조회 + * @param id : category id + * @return Category + */ + public Category getCategoryById(Integer id) { + + return categoryRepository.findById(id).orElseThrow(() + -> new NotFoundCategoryException("Category not found with id : " + id)); + } + + /** + * 카테고리 생성 + * @param name : 카테고리 이름 + * @return 생성된 카테고리 + */ + @Transactional + public Category createCategory(String name) { + try { + Category category = new Category(); + category.setName(name); + return categoryRepository.save(category); + } catch(Exception E) { + return null; + } + } + + /** + * 카테고리 이름 수정 + * @param id : 수정할 카테고리 아이디 + * @param name : 수정할 카테고리 이름 + * @return 수정된 카테고리 + */ + @Transactional + public Category updateCategory(Integer id, String name) { + Category category = findCategoryByIdOrThrow(id); + category.setName(name); + return category; + } + + /** + * 카테고리 삭제 + * @param id : 삭제할 카테고리 아이디 + * @return 성공 여부 + */ + @Transactional + public Boolean deleteCategory(Integer id) { + Category category = findCategoryByIdOrThrow(id); + try { + categoryRepository.delete(category); + return true; + } catch(Exception e) { + return false; + } + } + + private Category findCategoryByIdOrThrow(Integer id) { + return categoryRepository.findById(id) + .orElseThrow(() -> new NotFoundCategoryException("Category Not Found")); + } +} diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/application/ProductService.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/application/ProductService.java index f04e705..582aab9 100644 --- a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/application/ProductService.java +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/application/ProductService.java @@ -5,6 +5,7 @@ import java.util.stream.Collectors; import com.ssafy.springbootapi.domain.product.dao.ProductRepository; +import com.ssafy.springbootapi.domain.product.domain.Category; import com.ssafy.springbootapi.domain.product.domain.Product; import com.ssafy.springbootapi.domain.product.domain.ProductMapper; import com.ssafy.springbootapi.domain.product.dto.ProductInput; @@ -17,15 +18,14 @@ import com.ssafy.springbootapi.domain.user.exception.UserNotFoundException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; -import org.springframework.web.server.ResponseStatusException; @Service @RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; private final UserRepository userRepository; + private final CategoryService categoryService; private final ProductMapper productMapper; @@ -55,6 +55,7 @@ public ProductOutput getProductById(Long id) { * @param productInput : Product 생성 데이터 * @return ProductOutput(DTO) */ + @Transactional public ProductOutput insertProduct(ProductInput productInput) { // user_id로 User 객체 찾기 Optional userOptional = userRepository.findById(productInput.getUser_id()); @@ -62,11 +63,14 @@ public ProductOutput insertProduct(ProductInput productInput) { new UserNotFoundException("User not found with id: " + productInput.getUser_id()) ); + Category category = findCategoryByIdOrThrow(productInput.getCategory_id()); + // ProductInput에서 Product 엔티티로 변환 Product product = productMapper.toEntity(productInput); // User 객체를 Product 엔티티에 설정 product.setUser(user); + product.setCategory(category); // Product 엔티티를 DB에 저장하고, 저장된 엔티티를 ProductOutput DTO로 변환하여 반환 return productMapper.toProductOutput(productRepository.save(product)); @@ -78,6 +82,7 @@ public ProductOutput insertProduct(ProductInput productInput) { * @param id : 삭제할 product id * @return ProductOutput(DTO) */ + @Transactional public ProductOutput removeProduct(Long id) { Product toRemove = findProductByIdOrThrow(id); productRepository.delete(toRemove); @@ -93,6 +98,10 @@ public ProductOutput removeProduct(Long id) { @Transactional public ProductOutput updateProduct(Long id, ProductUpdate productUpdate) { Product productToUpdate = findProductByIdOrThrow(id); + if(productUpdate.getCategory_id() != null) { + Category category = findCategoryByIdOrThrow(productUpdate.getCategory_id()); + productToUpdate.setCategory(category); + } productMapper.updateProduct(productUpdate, productToUpdate); return productMapper.toProductOutput(productRepository.save(productToUpdate)); } @@ -106,4 +115,13 @@ private Product findProductByIdOrThrow(Long id) { return productRepository.findById(id) .orElseThrow(() -> new NotFoundProductException("Product not found with id: " + id)); } + + /** + * id로 카테고리 찾기. 없을 경우 Exception 발생 + * @param categoryId : 찾을 category 아이디 + * @return 카테고리 + */ + private Category findCategoryByIdOrThrow(Integer categoryId) { + return categoryService.getCategoryById(categoryId); + } } diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dao/CategoryRepository.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dao/CategoryRepository.java new file mode 100644 index 0000000..8897fa1 --- /dev/null +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dao/CategoryRepository.java @@ -0,0 +1,8 @@ +package com.ssafy.springbootapi.domain.product.dao; + +import com.ssafy.springbootapi.domain.product.domain.Category; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CategoryRepository extends JpaRepository { + +} diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/domain/Category.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/domain/Category.java new file mode 100644 index 0000000..61f39b7 --- /dev/null +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/domain/Category.java @@ -0,0 +1,23 @@ +package com.ssafy.springbootapi.domain.product.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "category") +@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) +public class Category { + @Id + @GeneratedValue + @Column(name = "id", unique = true) + private Integer id; + + @Column(name = "name") + private String name; +} diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/domain/Product.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/domain/Product.java index 38020aa..9d00673 100644 --- a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/domain/Product.java +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/domain/Product.java @@ -41,25 +41,24 @@ public class Product { private String name; @Column(name = "price", nullable = false) - private int price; + private Integer price; @Column(name = "description", nullable = false) private String description; - @Column(nullable = false) - private int category; + @ManyToOne + @JoinColumn(name="category_id", nullable = false) + private Category category; @Column(nullable = false) - private int stock; + private Integer stock; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; -// @Column(name = "user_id") -// private int userId; + // test code를 위해 추가 - public void updateInfo(int category, int stock, String imageUrl) { - this.category = category; + public void updateInfo(int stock, String imageUrl) { this.stock = stock; this.imageUrl = imageUrl; } diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductBase.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductBase.java index 961ce63..c336471 100644 --- a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductBase.java +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductBase.java @@ -10,5 +10,5 @@ public class ProductBase { private String imageUrl; private String name; - private int price; + private Integer price; } diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductInput.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductInput.java index 986d395..5c72375 100644 --- a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductInput.java +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductInput.java @@ -11,8 +11,9 @@ @SuperBuilder public class ProductInput extends ProductBase { private String description; - private int category; - private int stock; + @NotBlank(message = "category id is required") + private Integer category_id; + private Integer stock; @NotBlank(message = "user id is required") private Long user_id; } \ No newline at end of file diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductOutput.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductOutput.java index ba3dc8b..4462612 100644 --- a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductOutput.java +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductOutput.java @@ -1,7 +1,6 @@ package com.ssafy.springbootapi.domain.product.dto; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; @@ -12,7 +11,5 @@ @SuperBuilder public class ProductOutput extends ProductListOutput{ private String description; - private int category; private int stock; -// private int user_id; } diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductUpdate.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductUpdate.java index 473a6b1..555734b 100644 --- a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductUpdate.java +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/dto/ProductUpdate.java @@ -3,7 +3,6 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import lombok.experimental.SuperBuilder; @AllArgsConstructor @@ -12,6 +11,6 @@ @SuperBuilder public class ProductUpdate extends ProductBase { private String description; - private int category; - private int stock; + private Integer category_id; + private Integer stock; } diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/exception/NotFoundCategoryException.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/exception/NotFoundCategoryException.java new file mode 100644 index 0000000..ff9b675 --- /dev/null +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/domain/product/exception/NotFoundCategoryException.java @@ -0,0 +1,7 @@ +package com.ssafy.springbootapi.domain.product.exception; + +public class NotFoundCategoryException extends RuntimeException{ + public NotFoundCategoryException(String message) { + super(message); + } +} diff --git a/spring-boot-api/src/main/java/com/ssafy/springbootapi/global/error/GlobalExceptionHandler.java b/spring-boot-api/src/main/java/com/ssafy/springbootapi/global/error/GlobalExceptionHandler.java index 26f8a35..12f9ce4 100644 --- a/spring-boot-api/src/main/java/com/ssafy/springbootapi/global/error/GlobalExceptionHandler.java +++ b/spring-boot-api/src/main/java/com/ssafy/springbootapi/global/error/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package com.ssafy.springbootapi.global.error; + import com.ssafy.springbootapi.domain.product.exception.NotFoundCategoryException; import com.ssafy.springbootapi.domain.product.exception.NotFoundProductException; import com.ssafy.springbootapi.domain.user.exception.UserNotFoundException; import org.springframework.http.HttpStatus; @@ -15,6 +16,11 @@ public ResponseEntity handleNotFoundProductException(NotFoundProductException return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage()); } + @ExceptionHandler(NotFoundCategoryException.class) + public ResponseEntity handleNotFoundCategoryException(NotFoundCategoryException ex) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage()); + } + @ExceptionHandler(UserNotFoundException.class) public ResponseEntity handleUserNotFoundException(UserNotFoundException ex) { return ResponseEntity diff --git a/spring-boot-api/src/test/java/com/ssafy/springbootapi/domain/product/application/ProductServiceTest.java b/spring-boot-api/src/test/java/com/ssafy/springbootapi/domain/product/application/ProductServiceTest.java index e21ab2a..0480de2 100644 --- a/spring-boot-api/src/test/java/com/ssafy/springbootapi/domain/product/application/ProductServiceTest.java +++ b/spring-boot-api/src/test/java/com/ssafy/springbootapi/domain/product/application/ProductServiceTest.java @@ -1,6 +1,7 @@ package com.ssafy.springbootapi.domain.product.application; import com.ssafy.springbootapi.domain.product.dao.ProductRepository; +import com.ssafy.springbootapi.domain.product.domain.Category; import com.ssafy.springbootapi.domain.product.domain.Product; import com.ssafy.springbootapi.domain.product.domain.ProductMapper; import com.ssafy.springbootapi.domain.product.dto.ProductInput; @@ -32,6 +33,9 @@ class ProductServiceTest { @Mock private ProductRepository productRepository; + @Mock + private CategoryService categoryService; + @Mock private ProductMapper productMapper; @@ -93,16 +97,26 @@ class ProductServiceTest { void 상품삽입성공테스트() { // given Long userId = 1L; // 테스트용 user_id 값 + Integer categoryId = 1; // 테스트용 카테고리 id값 + ProductInput productInput = new ProductInput(); productInput.setUser_id(userId); // ProductInput에 user_id 설정 + productInput.setCategory_id(categoryId); + Product product = new Product(); User user = new User(); // 새 User 객체 생성 user.setId(userId); // User 객체에 id 설정 + + Category category = new Category(); // 새 카테고리 생성 + category.setId(categoryId); + ProductOutput productOutput = new ProductOutput(); when(userRepository.findById(userId)).thenReturn(java.util.Optional.of(user)); // userRepository.findById 호출 시 user 반환 + when(categoryService.getCategoryById(categoryId)).thenReturn(category); when(productMapper.toEntity(productInput)).thenReturn(product); product.setUser(user); // Product 객체에 User 설정 + product.setCategory(category); when(productRepository.save(product)).thenReturn(product); when(productMapper.toProductOutput(product)).thenReturn(productOutput); @@ -163,19 +177,17 @@ class ProductServiceTest { @Test void 상품수정성공테스트() { // given - int newCategory = 2; int newStock = 20; String newImageUrl = "newImageUrl"; ProductUpdate productUpdate = new ProductUpdate(); productUpdate.builder() - .category(newCategory) .stock(newStock) .imageUrl(newImageUrl).build(); Product product = new Product(); - product.updateInfo(newCategory, newStock, newImageUrl); + product.updateInfo(newStock, newImageUrl); ProductOutput productOutput = new ProductOutput(); @@ -190,7 +202,6 @@ class ProductServiceTest { // then assertThat(result).isEqualTo(productOutput); - assertThat(product.getCategory()).isEqualTo(newCategory); assertThat(product.getStock()).isEqualTo(newStock); assertThat(product.getImageUrl()).isEqualTo(newImageUrl); }