diff --git a/src/main/java/com/ongil/backend/domain/admin/controller/AdminController.java b/src/main/java/com/ongil/backend/domain/admin/controller/AdminController.java index a5ad91e..c01f960 100644 --- a/src/main/java/com/ongil/backend/domain/admin/controller/AdminController.java +++ b/src/main/java/com/ongil/backend/domain/admin/controller/AdminController.java @@ -1,14 +1,21 @@ package com.ongil.backend.domain.admin.controller; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.ongil.backend.domain.admin.dto.request.AdminBrandCreateRequest; +import com.ongil.backend.domain.admin.dto.request.AdminBrandUpdateRequest; import com.ongil.backend.domain.admin.dto.request.AdminCategoryCreateRequest; +import com.ongil.backend.domain.admin.dto.request.AdminCategoryUpdateRequest; import com.ongil.backend.domain.admin.dto.request.AdminProductCreateRequest; import com.ongil.backend.domain.admin.dto.request.AdminProductOptionCreateRequest; +import com.ongil.backend.domain.admin.dto.request.AdminProductOptionUpdateRequest; +import com.ongil.backend.domain.admin.dto.request.AdminProductUpdateRequest; import com.ongil.backend.domain.admin.service.AdminService; import com.ongil.backend.domain.brand.dto.response.BrandResponse; import com.ongil.backend.domain.category.dto.response.CategorySimpleResponse; @@ -57,4 +64,61 @@ public DataResponse createProductOption( ProductOptionResponse response = adminService.createProductOption(request); return DataResponse.from(response); } + + @Operation(summary = "브랜드 수정", description = "브랜드 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.") + @PatchMapping("/brands/{brandId}") + public DataResponse updateBrand( + @PathVariable Long brandId, + @RequestBody AdminBrandUpdateRequest request) { + BrandResponse response = adminService.updateBrand(brandId, request); + return DataResponse.from(response); + } + + @Operation(summary = "브랜드 삭제", description = "브랜드를 삭제합니다.") + @DeleteMapping("/brands/{brandId}") + public DataResponse deleteBrand(@PathVariable Long brandId) { + adminService.deleteBrand(brandId); + return DataResponse.from("브랜드가 삭제되었습니다."); + } + + @Operation(summary = "카테고리 수정", description = "카테고리 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.") + @PatchMapping("/categories/{categoryId}") + public DataResponse updateCategory( + @PathVariable Long categoryId, + @RequestBody AdminCategoryUpdateRequest request) { + CategorySimpleResponse response = adminService.updateCategory(categoryId, request); + return DataResponse.from(response); + } + + @Operation(summary = "카테고리 삭제", description = "카테고리를 삭제합니다.") + @DeleteMapping("/categories/{categoryId}") + public DataResponse deleteCategory(@PathVariable Long categoryId) { + adminService.deleteCategory(categoryId); + return DataResponse.from("카테고리가 삭제되었습니다."); + } + + @Operation(summary = "상품 수정", description = "상품 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.") + @PatchMapping("/products/{productId}") + public DataResponse updateProduct( + @PathVariable Long productId, + @RequestBody AdminProductUpdateRequest request) { + ProductSimpleResponse response = adminService.updateProduct(productId, request); + return DataResponse.from(response); + } + + @Operation(summary = "상품 옵션 수정", description = "상품 옵션 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.") + @PatchMapping("/product-options/{optionId}") + public DataResponse updateProductOption( + @PathVariable Long optionId, + @RequestBody AdminProductOptionUpdateRequest request) { + ProductOptionResponse response = adminService.updateProductOption(optionId, request); + return DataResponse.from(response); + } + + @Operation(summary = "상품 옵션 삭제", description = "상품 옵션을 삭제합니다.") + @DeleteMapping("/product-options/{optionId}") + public DataResponse deleteProductOption(@PathVariable Long optionId) { + adminService.deleteProductOption(optionId); + return DataResponse.from("상품 옵션이 삭제되었습니다."); + } } diff --git a/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminBrandUpdateRequest.java b/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminBrandUpdateRequest.java new file mode 100644 index 0000000..042b4f3 --- /dev/null +++ b/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminBrandUpdateRequest.java @@ -0,0 +1,20 @@ +package com.ongil.backend.domain.admin.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Schema(description = "브랜드 수정 요청") +public class AdminBrandUpdateRequest { + + @Schema(description = "브랜드명", example = "나이키") + private String name; + + @Schema(description = "브랜드 설명", example = "스포츠 의류 브랜드") + private String description; + + @Schema(description = "로고 이미지 URL", example = "https://example.com/logo.png") + private String logoImageUrl; +} diff --git a/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminCategoryUpdateRequest.java b/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminCategoryUpdateRequest.java new file mode 100644 index 0000000..00477de --- /dev/null +++ b/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminCategoryUpdateRequest.java @@ -0,0 +1,23 @@ +package com.ongil.backend.domain.admin.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Schema(description = "카테고리 수정 요청") +public class AdminCategoryUpdateRequest { + + @Schema(description = "카테고리명", example = "상의") + private String name; + + @Schema(description = "아이콘 URL", example = "https://example.com/icon.png") + private String iconUrl; + + @Schema(description = "정렬 순서", example = "1") + private Integer displayOrder; + + @Schema(description = "상위 카테고리 ID (하위 카테고리인 경우)", example = "1") + private Long parentCategoryId; +} diff --git a/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminProductOptionUpdateRequest.java b/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminProductOptionUpdateRequest.java new file mode 100644 index 0000000..1769bf6 --- /dev/null +++ b/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminProductOptionUpdateRequest.java @@ -0,0 +1,20 @@ +package com.ongil.backend.domain.admin.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Schema(description = "상품 옵션 수정 요청") +public class AdminProductOptionUpdateRequest { + + @Schema(description = "사이즈", example = "M") + private String size; + + @Schema(description = "색상", example = "화이트") + private String color; + + @Schema(description = "재고 수량", example = "100") + private Integer stock; +} diff --git a/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminProductUpdateRequest.java b/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminProductUpdateRequest.java new file mode 100644 index 0000000..524e6db --- /dev/null +++ b/src/main/java/com/ongil/backend/domain/admin/dto/request/AdminProductUpdateRequest.java @@ -0,0 +1,46 @@ +package com.ongil.backend.domain.admin.dto.request; + +import com.ongil.backend.domain.product.enums.ProductType; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Schema(description = "상품 수정 요청") +public class AdminProductUpdateRequest { + + @Schema(description = "상품명", example = "베이직 코튼 티셔츠") + private String name; + + @Schema(description = "상품 설명", example = "편안한 착용감의 코튼 티셔츠입니다.") + private String description; + + @Schema(description = "가격", example = "29000") + private Integer price; + + @Schema(description = "소재 정보", example = "면 100%") + private String materialOriginal; + + @Schema(description = "이미지 URL들 (쉼표로 구분)", example = "https://example.com/img1.jpg,https://example.com/img2.jpg") + private String imageUrls; + + @Schema(description = "사이즈들 (쉼표로 구분)", example = "S,M,L,XL") + private String sizes; + + @Schema(description = "색상들 (쉼표로 구분)", example = "화이트,블랙,네이비") + private String colors; + + @Schema(description = "할인율 (%)", example = "10") + private Integer discountRate; + + @Schema(description = "상품 타입", example = "NORMAL") + private ProductType productType; + + @Schema(description = "브랜드 ID", example = "1") + private Long brandId; + + @Schema(description = "카테고리 ID", example = "1") + private Long categoryId; +} diff --git a/src/main/java/com/ongil/backend/domain/admin/service/AdminService.java b/src/main/java/com/ongil/backend/domain/admin/service/AdminService.java index 7091fc9..d20f00f 100644 --- a/src/main/java/com/ongil/backend/domain/admin/service/AdminService.java +++ b/src/main/java/com/ongil/backend/domain/admin/service/AdminService.java @@ -4,9 +4,13 @@ import org.springframework.transaction.annotation.Transactional; import com.ongil.backend.domain.admin.dto.request.AdminBrandCreateRequest; +import com.ongil.backend.domain.admin.dto.request.AdminBrandUpdateRequest; import com.ongil.backend.domain.admin.dto.request.AdminCategoryCreateRequest; +import com.ongil.backend.domain.admin.dto.request.AdminCategoryUpdateRequest; import com.ongil.backend.domain.admin.dto.request.AdminProductCreateRequest; import com.ongil.backend.domain.admin.dto.request.AdminProductOptionCreateRequest; +import com.ongil.backend.domain.admin.dto.request.AdminProductOptionUpdateRequest; +import com.ongil.backend.domain.admin.dto.request.AdminProductUpdateRequest; import com.ongil.backend.domain.brand.converter.BrandConverter; import com.ongil.backend.domain.brand.dto.response.BrandResponse; import com.ongil.backend.domain.brand.entity.Brand; @@ -127,4 +131,137 @@ public ProductOptionResponse createProductOption(AdminProductOptionCreateRequest .stockStatus(savedOption.getStockStatus()) .build(); } + + // 브랜드 수정 + public BrandResponse updateBrand(Long brandId, AdminBrandUpdateRequest request) { + Brand brand = brandRepository.findById(brandId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.BRAND_NOT_FOUND)); + + brand.updateBrand( + request.getName(), + request.getDescription(), + request.getLogoImageUrl() + ); + + return brandConverter.toResponse(brand); + } + + // 브랜드 삭제 + public void deleteBrand(Long brandId) { + Brand brand = brandRepository.findById(brandId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.BRAND_NOT_FOUND)); + + // 해당 브랜드를 사용하는 상품이 있는지 확인 + if (productRepository.existsByBrandId(brandId)) { + throw new ValidationException(ErrorCode.CANNOT_DELETE_BRAND_WITH_PRODUCTS); + } + + brandRepository.delete(brand); + } + + // 카테고리 수정 + public CategorySimpleResponse updateCategory(Long categoryId, AdminCategoryUpdateRequest request) { + Category category = categoryRepository.findById(categoryId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.CATEGORY_NOT_FOUND)); + + Category parentCategory = null; + if (request.getParentCategoryId() != null) { + parentCategory = categoryRepository.findById(request.getParentCategoryId()) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.CATEGORY_NOT_FOUND)); + } + + category.updateCategory( + request.getName(), + request.getIconUrl(), + request.getDisplayOrder(), + parentCategory + ); + + return categoryConverter.toSimpleResponse(category); + } + + // 카테고리 삭제 + public void deleteCategory(Long categoryId) { + Category category = categoryRepository.findById(categoryId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.CATEGORY_NOT_FOUND)); + + // 해당 카테고리를 사용하는 상품이 있는지 확인 + if (productRepository.existsByCategoryId(categoryId)) { + throw new ValidationException(ErrorCode.CANNOT_DELETE_CATEGORY_WITH_PRODUCTS); + } + + // 하위 카테고리가 있는지 확인 + if (categoryRepository.existsByParentCategoryId(categoryId)) { + throw new ValidationException(ErrorCode.CANNOT_DELETE_CATEGORY_WITH_SUBCATEGORIES); + } + + categoryRepository.delete(category); + } + + // 상품 수정 + public ProductSimpleResponse updateProduct(Long productId, AdminProductUpdateRequest request) { + Product product = productRepository.findById(productId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.PRODUCT_NOT_FOUND)); + + Brand brand = null; + if (request.getBrandId() != null) { + brand = brandRepository.findById(request.getBrandId()) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.BRAND_NOT_FOUND)); + } + + Category category = null; + if (request.getCategoryId() != null) { + category = categoryRepository.findById(request.getCategoryId()) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.CATEGORY_NOT_FOUND)); + + // 상품은 하위 카테고리에만 등록 가능 + if (category.getParentCategory() == null) { + throw new ValidationException(ErrorCode.INVALID_CATEGORY); + } + } + + product.updateProduct( + request.getName(), + request.getDescription(), + request.getPrice(), + request.getMaterialOriginal(), + request.getImageUrls(), + request.getSizes(), + request.getColors(), + request.getDiscountRate(), + request.getProductType(), + brand, + category + ); + + return productConverter.toSimpleResponse(product); + } + + // 상품 옵션 수정 + public ProductOptionResponse updateProductOption(Long optionId, AdminProductOptionUpdateRequest request) { + ProductOption productOption = productOptionRepository.findById(optionId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.PRODUCT_OPTION_NOT_FOUND)); + + productOption.updateProductOption( + request.getSize(), + request.getColor(), + request.getStock() + ); + + return ProductOptionResponse.builder() + .optionId(productOption.getId()) + .size(productOption.getSize()) + .color(productOption.getColor()) + .stock(productOption.getStock()) + .stockStatus(productOption.getStockStatus()) + .build(); + } + + // 상품 옵션 삭제 + public void deleteProductOption(Long optionId) { + ProductOption productOption = productOptionRepository.findById(optionId) + .orElseThrow(() -> new EntityNotFoundException(ErrorCode.PRODUCT_OPTION_NOT_FOUND)); + + productOptionRepository.delete(productOption); + } } diff --git a/src/main/java/com/ongil/backend/domain/brand/entity/Brand.java b/src/main/java/com/ongil/backend/domain/brand/entity/Brand.java index 3bd5eec..0d998b0 100644 --- a/src/main/java/com/ongil/backend/domain/brand/entity/Brand.java +++ b/src/main/java/com/ongil/backend/domain/brand/entity/Brand.java @@ -34,4 +34,17 @@ public Brand(String name, String description, String logoImageUrl) { this.description = description; this.logoImageUrl = logoImageUrl; } + + // 브랜드 정보 수정 + public void updateBrand(String name, String description, String logoImageUrl) { + if (name != null) { + this.name = name; + } + if (description != null) { + this.description = description; + } + if (logoImageUrl != null) { + this.logoImageUrl = logoImageUrl; + } + } } \ No newline at end of file diff --git a/src/main/java/com/ongil/backend/domain/category/entity/Category.java b/src/main/java/com/ongil/backend/domain/category/entity/Category.java index 5205c27..1b8ade6 100644 --- a/src/main/java/com/ongil/backend/domain/category/entity/Category.java +++ b/src/main/java/com/ongil/backend/domain/category/entity/Category.java @@ -45,4 +45,26 @@ public Category(String name, String iconUrl, Integer displayOrder, Category pare this.displayOrder = displayOrder; this.parentCategory = parentCategory; } + + // 카테고리 정보 수정 + public void updateCategory(String name, String iconUrl, Integer displayOrder, Category parentCategory) { + if (name != null) { + this.name = name; + } + if (iconUrl != null) { + this.iconUrl = iconUrl; + } + if (displayOrder != null) { + this.displayOrder = displayOrder; + } + if (parentCategory != null) { + // 자기 참조(순환 참조) 방지 + if (parentCategory.getId().equals(this.id)) { + throw new com.ongil.backend.global.common.exception.ValidationException( + com.ongil.backend.global.common.exception.ErrorCode.CATEGORY_SELF_REFERENCE + ); + } + this.parentCategory = parentCategory; + } + } } \ No newline at end of file diff --git a/src/main/java/com/ongil/backend/domain/category/repository/CategoryRepository.java b/src/main/java/com/ongil/backend/domain/category/repository/CategoryRepository.java index e2f3a36..37ab8a4 100644 --- a/src/main/java/com/ongil/backend/domain/category/repository/CategoryRepository.java +++ b/src/main/java/com/ongil/backend/domain/category/repository/CategoryRepository.java @@ -34,4 +34,7 @@ public interface CategoryRepository extends JpaRepository { "WHERE c.parentCategory.id = :parentCategoryId " + "ORDER BY c.displayOrder") List findSubCategoriesByParentId(@Param("parentCategoryId") Long parentCategoryId); + + // 특정 카테고리를 상위 카테고리로 가진 하위 카테고리가 있는지 확인 + boolean existsByParentCategoryId(Long parentCategoryId); } \ No newline at end of file diff --git a/src/main/java/com/ongil/backend/domain/product/entity/Product.java b/src/main/java/com/ongil/backend/domain/product/entity/Product.java index 642f9ef..31b826b 100644 --- a/src/main/java/com/ongil/backend/domain/product/entity/Product.java +++ b/src/main/java/com/ongil/backend/domain/product/entity/Product.java @@ -136,4 +136,53 @@ public void updateAiMaterialDescription( public Integer getEffectivePrice() { return (discountPrice != null && discountPrice > 0) ? discountPrice : price; } + + // 상품 정보 수정 + public void updateProduct(String name, String description, Integer price, String materialOriginal, + String imageUrls, String sizes, String colors, Integer discountRate, ProductType productType, + Brand brand, Category category) { + if (name != null) { + this.name = name; + } + if (description != null) { + this.description = description; + } + if (price != null) { + this.price = price; + // 가격 변경 시 할인가 재계산 + if (this.discountRate != null && this.discountRate > 0) { + this.discountPrice = price - (price * this.discountRate / 100); + } + } + if (materialOriginal != null) { + this.materialOriginal = materialOriginal; + } + if (imageUrls != null) { + this.imageUrls = imageUrls; + } + if (sizes != null) { + this.sizes = sizes; + } + if (colors != null) { + this.colors = colors; + } + if (discountRate != null) { + this.discountRate = discountRate; + // 할인율 변경 시 할인가 재계산 + if (this.price != null && discountRate > 0) { + this.discountPrice = this.price - (this.price * discountRate / 100); + } else { + this.discountPrice = null; + } + } + if (productType != null) { + this.productType = productType; + } + if (brand != null) { + this.brand = brand; + } + if (category != null) { + this.category = category; + } + } } \ No newline at end of file diff --git a/src/main/java/com/ongil/backend/domain/product/entity/ProductOption.java b/src/main/java/com/ongil/backend/domain/product/entity/ProductOption.java index 11fe83b..5c00a1d 100644 --- a/src/main/java/com/ongil/backend/domain/product/entity/ProductOption.java +++ b/src/main/java/com/ongil/backend/domain/product/entity/ProductOption.java @@ -45,6 +45,25 @@ public StockStatus getStockStatus() { return this.stock == 0 ? StockStatus.SOLD_OUT : StockStatus.AVAILABLE; } + // 상품 옵션 수정 + public void updateProductOption(String size, String color, Integer stock) { + if (size != null) { + this.size = size; + } + if (color != null) { + this.color = color; + } + if (stock != null) { + // 재고 음수 방지 + if (stock < 0) { + throw new com.ongil.backend.global.common.exception.ValidationException( + com.ongil.backend.global.common.exception.ErrorCode.INVALID_STOCK + ); + } + this.stock = stock; + } + } + public enum StockStatus { AVAILABLE, // 구매 가능 SOLD_OUT // 품절 diff --git a/src/main/java/com/ongil/backend/domain/product/repository/ProductOptionRepository.java b/src/main/java/com/ongil/backend/domain/product/repository/ProductOptionRepository.java index dff67e0..45ac465 100644 --- a/src/main/java/com/ongil/backend/domain/product/repository/ProductOptionRepository.java +++ b/src/main/java/com/ongil/backend/domain/product/repository/ProductOptionRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.JpaRepository; +import com.ongil.backend.domain.product.entity.Product; import com.ongil.backend.domain.product.entity.ProductOption; public interface ProductOptionRepository extends JpaRepository { @@ -23,4 +24,7 @@ public interface ProductOptionRepository extends JpaRepository findByProductIdAndStockGreaterThan(Long productId, int stock); + + // 특정 상품의 모든 옵션 삭제 + void deleteByProduct(Product product); } diff --git a/src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java b/src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java index ced5bcf..89ab922 100644 --- a/src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java +++ b/src/main/java/com/ongil/backend/domain/product/repository/ProductRepository.java @@ -229,4 +229,10 @@ List findRecommendedProducts( @EntityGraph(attributePaths = {"brand", "category"}) @Query("SELECT p FROM Product p WHERE p.id IN :productIds AND p.onSale = true") List findByIdInAndOnSaleTrue(@Param("productIds") List productIds); + + // 특정 브랜드를 사용하는 상품이 있는지 확인 + boolean existsByBrandId(Long brandId); + + // 특정 카테고리를 사용하는 상품이 있는지 확인 + boolean existsByCategoryId(Long categoryId); } \ No newline at end of file diff --git a/src/main/java/com/ongil/backend/global/common/exception/ErrorCode.java b/src/main/java/com/ongil/backend/global/common/exception/ErrorCode.java index b27b297..2f3f8e2 100644 --- a/src/main/java/com/ongil/backend/global/common/exception/ErrorCode.java +++ b/src/main/java/com/ongil/backend/global/common/exception/ErrorCode.java @@ -33,13 +33,18 @@ public enum ErrorCode { PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "상품을 찾을 수 없습니다.", "PRODUCT-001"), PRODUCT_OPTION_NOT_FOUND(HttpStatus.NOT_FOUND, "선택한 옵션(색상/사이즈)을 찾을 수 없습니다.", "PRODUCT-002"), OUT_OF_STOCK(HttpStatus.BAD_REQUEST, "재고가 부족합니다.", "PRODUCT-003"), + INVALID_STOCK(HttpStatus.BAD_REQUEST, "재고는 0 이상이어야 합니다.", "PRODUCT-004"), // BRAND BRAND_NOT_FOUND(HttpStatus.NOT_FOUND, "브랜드를 찾을 수 없습니다.", "BRAND-001"), + CANNOT_DELETE_BRAND_WITH_PRODUCTS(HttpStatus.BAD_REQUEST, "해당 브랜드를 사용하는 상품이 있어 삭제할 수 없습니다.", "BRAND-002"), // CATEGORY CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "카테고리를 찾을 수 없습니다.", "CATEGORY-001"), INVALID_CATEGORY(HttpStatus.BAD_REQUEST, "상품은 하위 카테고리에만 등록할 수 있습니다.", "CATEGORY-002"), + CANNOT_DELETE_CATEGORY_WITH_PRODUCTS(HttpStatus.BAD_REQUEST, "해당 카테고리를 사용하는 상품이 있어 삭제할 수 없습니다.", "CATEGORY-003"), + CANNOT_DELETE_CATEGORY_WITH_SUBCATEGORIES(HttpStatus.BAD_REQUEST, "하위 카테고리가 있어 삭제할 수 없습니다.", "CATEGORY-004"), + CATEGORY_SELF_REFERENCE(HttpStatus.BAD_REQUEST, "카테고리의 상위 카테고리로 자기 자신을 설정할 수 없습니다.", "CATEGORY-005"), // WISHLIST WISHLIST_NOT_FOUND(HttpStatus.NOT_FOUND, "찜을 찾을 수 없습니다.", "WISHLIST-001"),