초기에 카테고리 변경사항이 없을것으로 생각하고 Enum 으로 처리했었다.
하지만 관리자기능 등을 새로 구현하려고 보니 카테고리의 추가 제거 수정 등이 필요해보였다.
기존의 Enum 으로 처리했을때는 이것이 불가능하여 새로 Category Entity 를 만들고 모두 변경해주었다.
package com.hello.foreverpet.domain.entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ManyToOne;
import com.hello.foreverpet.auditing.BaseTimeEntity;
import com.hello.foreverpet.domain.dto.request.UpdateProductRequest;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Product extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long productId;
@NotNull
private String productName;
@NotNull
@Column(length = 500)
private String productDescription;
// @NotNull
// @Enumerated(EnumType.STRING)
// private Categories categories;
@NotNull
@ManyToOne(fetch = FetchType.LAZY)
private Category category;
@NotNull
private Long productPrice;
private Long numberOfSold;
private String productImage;
@NotNull
private String brandName;
@ManyToOne(fetch = FetchType.LAZY)
private Wish wish;
// @Builder
// public Product(String productName, String productDescription, Categories categories, Long productPrice,
// String productImage, String brandName) {
// this.productName = productName;
// this.productDescription = productDescription;
// this.categories = categories;
// this.productPrice = productPrice;
// this.numberOfSold = 0L;
// this.productImage = productImage;
// this.brandName = brandName;
// }
@Builder
public Product(String productName, String productDescription, Category category, Long productPrice,
String productImage, String brandName) {
this.productName = productName;
this.productDescription = productDescription;
this.category = category;
this.productPrice = productPrice;
this.numberOfSold = 0L;
this.productImage = productImage;
this.brandName = brandName;
}
public Product updateProductByUpdateRequest(UpdateProductRequest updateProductRequest) {
this.productName = updateProductRequest.getProductName();
this.productDescription = updateProductRequest.getProductDescription();
this.category = new Category();
this.productPrice = updateProductRequest.getProductPrice();
this.productImage = updateProductRequest.getProductImage();
this.brandName = updateProductRequest.getBrandName();
return this;
}
public void setWish(Wish wish) {
this.wish = wish;
if (wish != null && !wish.getProducts().contains(this)) {
wish.getProducts().add(this);
}
}
// 주문시 판매수량 증가
public void soldProducts() {
this.numberOfSold++;
}
// 주문 취소시 판매수량 감소
public void cancelOrder() {
if (numberOfSold > 1) {
this.numberOfSold--;
}
}
}
@Operation(summary = "카테고리 추가", description = "상품 카테고리를 추가합니다.")
@PostMapping("/admin/category/add")
public void newCategory(@RequestParam String category) {
categoryService.addCategory(category);
}
@Operation(summary = "카테고리 수정", description = "상품 카테고리를 수정합니다.")
@PostMapping("/admin/category/update/{id}")
public void updateCategory(@PathVariable Long id, @RequestParam String category) {
categoryService.updateCategory(id, category);
}
@Operation(summary = "카테고리 삭제", description = "상품 카테고리를 삭제합니다.")
@PostMapping("/admin/category/delete")
public void deleteCategory(@RequestParam String category) {
categoryService.deleteCategory(category);
}
public void categoryInit() {
String[] categories = {"SNACK", "BITA", "FOOD"};
for (String category : categories) {
Category byName = categoryService.findByName(category);
if (byName == null) {
categoryService.addCategory(category);
}
}
}
기존 존재하던 세개의 카테고리는 InitMethod 를 변경하며 추가해주었다.
Entity 로 존재하니 새로운 상품을 추가할때 String 으로 그 값을 입력받아 findByName 으로 찾도록 했고
프론트에서 상품 카테고리를 입력할때 모든 카테고리 리스트를 보내주어 존재하는 카테고리만 입력받게 작업하고
한번더 백엔드에서도 존재하는 카테고리 인지 여부를 체크해주면 좋을거같다.
package com.hello.foreverpet.domain.dto.request;
import com.hello.foreverpet.domain.entity.Category;
import com.hello.foreverpet.domain.entity.Product;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
@Data
@Schema(description = "상품 등록 요청DTO")
public class NewProductRequest {
@Schema(description = "등록할 상품 이름",example = "상품1")
@NotNull
private String productName;
@Schema(description = "등록할 상품 설명",example = "이 상품을 구매하세요")
@NotNull
private String productDescription;
@Schema(description = "등록할 상품 카테고리",defaultValue = "SNACK",allowableValues = {"SNACK","BITA","FOOD"})
@NotNull
private String categories;
@Schema(description = "등록할 상품 가격",example = "5000")
@NotNull
private Long productPrice;
@Schema(description = "등록할 상품 이미지 주소",example = "http://forevepet/image/1")
@NotNull
private String productImage;
@Schema(description = "등록할 상품 브랜드",example = "애플")
@NotNull
private String brandName;
public Product toEntity(Category category) {
return Product.builder().productName(this.productName)
.productDescription(this.productDescription)
.category(category)
.productPrice(this.productPrice)
.productImage(this.productImage)
.brandName(this.brandName)
.build();
}
@Builder
public NewProductRequest(String productName, String productDescription, String categories, Long productPrice,
String productImage,String brandName) {
this.productName = productName;
this.productDescription = productDescription;
this.categories = categories;
this.productPrice = productPrice;
this.productImage = productImage;
this.brandName = brandName;
}
}
Response 는 String 으로 Category 의 Name 을 조회하여 보내주도록 하였다.
package com.hello.foreverpet.domain.dto.response;
import com.hello.foreverpet.domain.entity.Product;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import lombok.Data;
@Data
@Schema(description = "상품 응답")
public class ProductResponse {
private Long id;
private String productName;
private String productDescription;
private String categoryName;
private Long productPrice;
private Long numberOfSold;
private String productImage;
private String brandName;
private LocalDateTime createdDate;
private LocalDateTime modifiedDate;
public ProductResponse(Product product) {
this.id = product.getProductId();
this.productName = product.getProductName();
this.productDescription = product.getProductDescription();
this.categoryName = product.getCategory().getName();
this.productPrice = product.getProductPrice();
this.numberOfSold = product.getNumberOfSold();
this.productImage = product.getProductImage();
this.brandName = product.getBrandName();
this.createdDate = product.getCreateDate();
this.modifiedDate = product.getModifiedDate();
}
}
package com.hello.foreverpet.service;
import com.hello.foreverpet.domain.dto.request.NewProductRequest;
import com.hello.foreverpet.domain.dto.request.UpdateProductRequest;
import com.hello.foreverpet.domain.dto.response.LoginUserProductResponse;
import com.hello.foreverpet.domain.dto.response.ProductResponse;
import com.hello.foreverpet.domain.entity.CartProduct;
import com.hello.foreverpet.domain.entity.Category;
import com.hello.foreverpet.domain.entity.Product;
import com.hello.foreverpet.domain.entity.UserInfo;
import com.hello.foreverpet.domain.exception.AlreadyExistProductException;
import com.hello.foreverpet.domain.exception.CategoryNotFoundException;
import com.hello.foreverpet.domain.exception.ProductNotFoundException;
import com.hello.foreverpet.domain.exception.UserNotFoundException;
import com.hello.foreverpet.handler.ErrorCode;
import com.hello.foreverpet.jwt.TokenProvider;
import com.hello.foreverpet.repository.CategoryRepository;
import com.hello.foreverpet.repository.CustomProductRepository;
import com.hello.foreverpet.repository.ProductJpaRepository;
import com.hello.foreverpet.repository.UserInfoJpaRepository;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductJpaRepository productJpaRepository;
private final CustomProductRepository customProductRepository;
private final UserInfoJpaRepository userInfoJpaRepository;
private final TokenProvider tokenProvider;
private final CategoryRepository categoryRepository;
@Transactional
public ProductResponse createProduct(NewProductRequest newProductRequest) {
if (productJpaRepository.findByProductName(newProductRequest.getProductName()).isPresent()) {
throw new AlreadyExistProductException(ErrorCode.ALREADY_EXIST_PRODUCT_EXCEPTION);
}
Category category = categoryRepository.findByName(newProductRequest.getCategories())
.orElseThrow(() -> new CategoryNotFoundException(ErrorCode.CATEGORY_NOT_FOUND));
Product newProduct = newProductRequest.toEntity(category);
productJpaRepository.save(newProduct);
return new ProductResponse(newProduct);
}
public List<ProductResponse> getAllProducts() {
return productJpaRepository.findAll().stream().map(ProductResponse::new).toList();
}
@Transactional
public ProductResponse updateProduct(Long id, UpdateProductRequest updateProductRequest) {
return productJpaRepository.findById(id)
.map(product -> {
Product updateProduct = product.updateProductByUpdateRequest(updateProductRequest);
return new ProductResponse(updateProduct);
})
.orElseThrow(() -> new ProductNotFoundException(ErrorCode.PRODUCT_FOUND_ERROR));
}
public Long deleteProduct(Long id) {
return productJpaRepository.findById(id)
.map(product -> {
productJpaRepository.delete(product);
return id;
})
.orElseThrow(() -> new ProductNotFoundException(ErrorCode.PRODUCT_FOUND_ERROR));
}
public ProductResponse findProductById(Long id) {
return productJpaRepository.findById(id).map(ProductResponse::new).orElseThrow(IllegalArgumentException::new);
}
public List<ProductResponse> searchProduct(String search) {
return customProductRepository.findProductBySearch(search).stream()
.map(ProductResponse::new).toList();
}
public List<ProductResponse> orderBySold() {
return customProductRepository.findProductOrderBySold().stream()
.map(ProductResponse::new).toList();
}
public List<ProductResponse> orderByNew() {
return customProductRepository.findProductOrderByNew().stream()
.map(ProductResponse::new).toList();
}
public List<ProductResponse> productByCategories(String categoryName) {
return customProductRepository.findProductByCategories(categoryName).stream()
.map(ProductResponse::new).toList();
}
public List<LoginUserProductResponse> loginUserGetAllProducts(HttpServletRequest httpServletRequest) {
UserInfo userInfo = getUserInfo(httpServletRequest);
List<Product> cart = getCartProducts(userInfo.getUserId()).stream().map(CartProduct::getProduct)
.toList();
List<Product> wish = getWishProducts(userInfo.getUserId());
return productJpaRepository.findAll().stream()
.map(product -> {
LoginUserProductResponse loginUserProductResponse = new LoginUserProductResponse(product);
if (cart.contains(product)) {
loginUserProductResponse.reverseInCart();
}
if (wish.contains(product)) {
loginUserProductResponse.reverseInWish();
}
return loginUserProductResponse;
})
.toList();
}
private List<CartProduct> getCartProducts(Long userId) {
return userInfoJpaRepository.findById(userId)
.map(userInfo -> userInfo.getCart().getCartProducts())
.orElse(Collections.emptyList());
}
private List<Product> getWishProducts(Long userId) {
return userInfoJpaRepository.findById(userId)
.map(userInfo -> userInfo.getWish().getProducts())
.orElse(Collections.emptyList());
}
private UserInfo getUserInfo(HttpServletRequest httpServletRequest) {
String token = httpServletRequest.getHeader("Authorization");
return userInfoJpaRepository.findById(
Long.valueOf(tokenProvider.getAuthentication(token).getName())).orElseThrow(() -> new UserNotFoundException(ErrorCode.USER_NOT_FOUND));
}
}
package com.hello.foreverpet.domain.exception;
import com.hello.foreverpet.handler.ErrorCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class CategoryNotFoundException extends IllegalArgumentException{
private final ErrorCode errorCode;
}
package com.hello.foreverpet.domain.exception;
import com.hello.foreverpet.handler.ErrorCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class AlreadyExistCategoryException extends IllegalArgumentException{
private final ErrorCode errorCode;
}
@ExceptionHandler(CategoryNotFoundException.class)
public ResponseEntity<ErrorResponse> categoryNotFoundExceptionHandler(CategoryNotFoundException e) {
ErrorResponse response = new ErrorResponse(ErrorCode.CATEGORY_NOT_FOUND);
return new ResponseEntity<>(response, HttpStatusCode.valueOf(e.getErrorCode().getStatus()));
}
@ExceptionHandler(AlreadyExistCategoryException.class)
public ResponseEntity<ErrorResponse> alreadyExistCategoryExceptionHandler(AlreadyExistCategoryException e) {
ErrorResponse response = new ErrorResponse(ErrorCode.CATEGORY_ALREADY_EXIST);
return new ResponseEntity<>(response, HttpStatusCode.valueOf(e.getErrorCode().getStatus()));
}
카테고리 관련 처리는 모두 잘 되었다.
하지만 한가지 문제가 남아있었는데
카테고리를 삭제할 경우 해당 카테고리의 상품들을 어떻게 처리해야할까 ??
기존 카테고리가 삭제되었을 경우 상품에 포함되어 있는 카테고리도 사라지게 된다.
그래서 생각난 해결방법은 카테고리를 삭제하지 않고 활성 / 비활성 으로 나누어서 관리하면 어떨까 였다.
어짜피 각 상품에 대한 카테고리가 변경된다면 관리자가 수정해주어야 하기때문에 이방법으로 우선 수정해두어야겠다.