Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -57,4 +64,61 @@ public DataResponse<ProductOptionResponse> createProductOption(
ProductOptionResponse response = adminService.createProductOption(request);
return DataResponse.from(response);
}

@Operation(summary = "브랜드 수정", description = "브랜드 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.")
@PatchMapping("/brands/{brandId}")
public DataResponse<BrandResponse> 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<String> deleteBrand(@PathVariable Long brandId) {
adminService.deleteBrand(brandId);
return DataResponse.from("브랜드가 삭제되었습니다.");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Operation(summary = "카테고리 수정", description = "카테고리 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.")
@PatchMapping("/categories/{categoryId}")
public DataResponse<CategorySimpleResponse> 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<String> deleteCategory(@PathVariable Long categoryId) {
adminService.deleteCategory(categoryId);
return DataResponse.from("카테고리가 삭제되었습니다.");
}

@Operation(summary = "상품 수정", description = "상품 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.")
@PatchMapping("/products/{productId}")
public DataResponse<ProductSimpleResponse> 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<ProductOptionResponse> 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<String> deleteProductOption(@PathVariable Long optionId) {
adminService.deleteProductOption(optionId);
return DataResponse.from("상품 옵션이 삭제되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +9 to +45
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

onSale 필드 누락 — PR 목적과 불일치

커밋 메시지에 "주문 이력 보존을 위해 상품의 물리 삭제 기능 제거. 상품 판매 중단은 상품 수정 API를 통해 onSale=false로 처리하도록 변경"이라고 명시되어 있으나, 이 DTO에 onSale (또는 Boolean 타입의 판매 상태) 필드가 없습니다. 현재 상태로는 상품 판매 중단 기능이 동작하지 않습니다.

🐛 onSale 필드 추가 제안
 	`@Schema`(description = "카테고리 ID", example = "1")
 	private Long categoryId;
+
+	`@Schema`(description = "판매 여부", example = "true")
+	private Boolean onSale;
 }

As per coding guidelines, DTO 설계 검증: "누락된 필드 - API 응답에 필요한 필드가 빠져있는지 확인".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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;
`@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;
`@Schema`(description = "판매 여부", example = "true")
private Boolean onSale;
}
🤖 Prompt for AI Agents
In
`@src/main/java/com/ongil/backend/domain/admin/dto/request/AdminProductUpdateRequest.java`
around lines 9 - 45, The AdminProductUpdateRequest DTO is missing the onSale
field required to toggle product sale status; add a private Boolean onSale field
to the AdminProductUpdateRequest class with an appropriate `@Schema`(description =
"판매 여부", example = "false") annotation so the API can accept onSale updates (the
class already uses `@Getter/`@NoArgsConstructor so no extra accessor code is
needed).

}
137 changes: 137 additions & 0 deletions src/main/java/com/ongil/backend/domain/admin/service/AdminService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Comment on lines +167 to +171
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# AdminService.java의 updateCategory 메서드 전체 확인
fd "AdminService.java" --type f -x cat {}

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 9362


🏁 Script executed:

# Category.java 찾기 및 updateCategory 메서드 확인
fd "Category.java" --type f -x cat {}

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 1924


🏁 Script executed:

# ProductOptionResponse 확인 - converter 패턴 사용 여부
rg "ProductOptionResponse" --type java -A 5 | head -60

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 6463


🏁 Script executed:

# ProductConverter 전체 내용 확인
fd "ProductConverter.java" --type f -x cat {}

Repository: IT-Cotato/12th-OnGil-BE

Length of output: 4821


parentCategoryIdnull일 때 상위 카테고리를 해제할 수 없습니다.

Category.updateCategory 메서드가 if (parentCategory != null) 조건으로만 업데이트하므로, 요청의 parentCategoryIdnull이면 현재 값이 유지됩니다. 이는 PATCH 시맨틱의 "변경하지 않음" 의미로는 맞지만, 하위 카테고리를 최상위 카테고리로 승격하려는 의도와 구분할 수 없습니다.

명시적인 "해제" 신호(예: 0L 또는 전용 필드)를 도입하여 두 의도를 명확히 구분하는 것이 안전합니다.

🤖 Prompt for AI Agents
In `@src/main/java/com/ongil/backend/domain/admin/service/AdminService.java`
around lines 162 - 166, The current logic in AdminService (handling
request.getParentCategoryId()) treats a null parentCategoryId as "do not change"
because Category.updateCategory only updates when parentCategory != null; to
allow explicit removal of a parent, add a clear signal in the request (e.g.,
treat parentCategoryId == 0L or add a boolean like clearParent) and branch: if
request.getParentCategoryId() == 0L (or clearParent==true) set parentCategory =
null and mark that parent should be cleared, else if parentCategoryId != null
load parent via categoryRepository.findById(...); then call
Category.updateCategory with a parameter or overload that can distinguish
"no-op", "set to X", and "clear parent" (or pass an Optional/flag) so
updateCategory can explicitly remove the parent when requested.


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);
}
Comment on lines +201 to +238
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

updateProduct에서 onSale 필드가 전달되지 않습니다.

PR 목표에 명시된 "상품 판매 중단은 상품 수정 API를 통해 onSale=false로 처리"하는 기능이 현재 구현에서 누락되어 있습니다. AdminProductUpdateRequestonSale 필드를 추가하고 product.updateProduct(...) 호출 시 함께 전달해야 합니다.

🤖 Prompt for AI Agents
In `@src/main/java/com/ongil/backend/domain/admin/service/AdminService.java`
around lines 186 - 223, The updateProduct method is not passing the onSale flag
from AdminProductUpdateRequest into product.updateProduct; add an onSale Boolean
to AdminProductUpdateRequest (with getter) if missing, update the call site in
AdminService.updateProduct to pass request.getOnSale() into
product.updateProduct, and ensure the Product.updateProduct signature (and any
usages) is updated to accept and apply the onSale value so the Product's onSale
state is persisted; keep other validations unchanged and return
productConverter.toSimpleResponse(product) as before.


// 상품 옵션 수정
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);
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/ongil/backend/domain/brand/entity/Brand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment on lines +60 to +68
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

간접 순환 참조(A→B→A) 감지 누락

현재 자기 참조 방지 로직은 직접적인 자기 참조(parentCategory.id == this.id)만 검사합니다. 카테고리 A의 부모를 B로, B의 부모를 A로 설정하는 간접 순환 참조는 감지하지 못합니다. 카테고리 트리 깊이가 얕다면 당장 문제가 되지 않을 수 있지만, 향후 트리 순회 시 무한 루프 위험이 있습니다.

간접 순환을 방지하려면 부모 체인을 따라 올라가며 현재 카테고리가 이미 존재하는지 확인하는 로직이 필요합니다.

♻️ 순환 참조 방어 코드 예시
 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
-        );
+        throw new ValidationException(ErrorCode.CATEGORY_SELF_REFERENCE);
+    }
+    // 간접 순환 참조 방지
+    Category ancestor = parentCategory.getParentCategory();
+    while (ancestor != null) {
+        if (ancestor.getId().equals(this.id)) {
+            throw new ValidationException(ErrorCode.CATEGORY_SELF_REFERENCE);
+        }
+        ancestor = ancestor.getParentCategory();
     }
     this.parentCategory = parentCategory;
 }

참고: 이 체크가 Entity 내부에서 동작하려면 부모 체인이 이미 fetch된 상태여야 합니다. Lazy 로딩 환경에서는 AdminService 레벨에서 검증하는 것이 더 안전할 수 있습니다.

🤖 Prompt for AI Agents
In `@src/main/java/com/ongil/backend/domain/category/entity/Category.java` around
lines 60 - 68, The current self-reference check in Category only compares
parentCategory.getId() to this.id and misses indirect cycles (A→B→A); update the
Category logic that sets parentCategory (e.g., the constructor or
setParentCategory method) to walk the parent chain via
parentCategory.getParentCategory() (using getParentCategory()/getId()) until
null, and if any ancestor's id equals this.id throw the same ValidationException
with ErrorCode.CATEGORY_SELF_REFERENCE; ensure the traversal stops when parent
is null to avoid infinite loops and consider adding this check at the service
layer if parent relations may be lazily unloaded.

}
Comment on lines +48 to +69
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

자기 참조(순환 참조) 방지 로직 누락

parentCategory로 자기 자신을 전달하면 순환 참조가 발생하여 카테고리 트리 조회 시 무한 루프에 빠질 수 있습니다. 또한, parentCategorynull로 설정하여 최상위 카테고리로 변경하는 것이 현재 null-check 패턴으로는 불가능합니다.

🛡️ 순환 참조 방어 코드 제안
 if (parentCategory != null) {
+    if (parentCategory.getId().equals(this.id)) {
+        throw new IllegalArgumentException("자기 자신을 상위 카테고리로 설정할 수 없습니다.");
+    }
     this.parentCategory = parentCategory;
 }
🤖 Prompt for AI Agents
In `@src/main/java/com/ongil/backend/domain/category/entity/Category.java` around
lines 48 - 63, The updateCategory method in Category allows setting
parentCategory only when non-null and lacks self-reference protection; change
updateCategory in class Category so it accepts null to unset the parent
(allowing promotion to top-level) and explicitly prevent cycles by rejecting
when parentCategory == this (e.g., throw IllegalArgumentException); keep the
other field updates unchanged and reference updateCategory and parentCategory
when making the fix.

}
Loading