Skip to content

Commit 2a84f34

Browse files
authored
Merge pull request #103 from IT-Cotato/feature/102
[Feat] 관리자 기능 추가
2 parents 4038dce + ca172a9 commit 2a84f34

File tree

14 files changed

+431
-0
lines changed

14 files changed

+431
-0
lines changed

src/main/java/com/ongil/backend/domain/admin/controller/AdminController.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package com.ongil.backend.domain.admin.controller;
22

3+
import org.springframework.web.bind.annotation.DeleteMapping;
4+
import org.springframework.web.bind.annotation.PatchMapping;
5+
import org.springframework.web.bind.annotation.PathVariable;
36
import org.springframework.web.bind.annotation.PostMapping;
47
import org.springframework.web.bind.annotation.RequestBody;
58
import org.springframework.web.bind.annotation.RequestMapping;
69
import org.springframework.web.bind.annotation.RestController;
710

811
import com.ongil.backend.domain.admin.dto.request.AdminBrandCreateRequest;
12+
import com.ongil.backend.domain.admin.dto.request.AdminBrandUpdateRequest;
913
import com.ongil.backend.domain.admin.dto.request.AdminCategoryCreateRequest;
14+
import com.ongil.backend.domain.admin.dto.request.AdminCategoryUpdateRequest;
1015
import com.ongil.backend.domain.admin.dto.request.AdminProductCreateRequest;
1116
import com.ongil.backend.domain.admin.dto.request.AdminProductOptionCreateRequest;
17+
import com.ongil.backend.domain.admin.dto.request.AdminProductOptionUpdateRequest;
18+
import com.ongil.backend.domain.admin.dto.request.AdminProductUpdateRequest;
1219
import com.ongil.backend.domain.admin.service.AdminService;
1320
import com.ongil.backend.domain.brand.dto.response.BrandResponse;
1421
import com.ongil.backend.domain.category.dto.response.CategorySimpleResponse;
@@ -57,4 +64,61 @@ public DataResponse<ProductOptionResponse> createProductOption(
5764
ProductOptionResponse response = adminService.createProductOption(request);
5865
return DataResponse.from(response);
5966
}
67+
68+
@Operation(summary = "브랜드 수정", description = "브랜드 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.")
69+
@PatchMapping("/brands/{brandId}")
70+
public DataResponse<BrandResponse> updateBrand(
71+
@PathVariable Long brandId,
72+
@RequestBody AdminBrandUpdateRequest request) {
73+
BrandResponse response = adminService.updateBrand(brandId, request);
74+
return DataResponse.from(response);
75+
}
76+
77+
@Operation(summary = "브랜드 삭제", description = "브랜드를 삭제합니다.")
78+
@DeleteMapping("/brands/{brandId}")
79+
public DataResponse<String> deleteBrand(@PathVariable Long brandId) {
80+
adminService.deleteBrand(brandId);
81+
return DataResponse.from("브랜드가 삭제되었습니다.");
82+
}
83+
84+
@Operation(summary = "카테고리 수정", description = "카테고리 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.")
85+
@PatchMapping("/categories/{categoryId}")
86+
public DataResponse<CategorySimpleResponse> updateCategory(
87+
@PathVariable Long categoryId,
88+
@RequestBody AdminCategoryUpdateRequest request) {
89+
CategorySimpleResponse response = adminService.updateCategory(categoryId, request);
90+
return DataResponse.from(response);
91+
}
92+
93+
@Operation(summary = "카테고리 삭제", description = "카테고리를 삭제합니다.")
94+
@DeleteMapping("/categories/{categoryId}")
95+
public DataResponse<String> deleteCategory(@PathVariable Long categoryId) {
96+
adminService.deleteCategory(categoryId);
97+
return DataResponse.from("카테고리가 삭제되었습니다.");
98+
}
99+
100+
@Operation(summary = "상품 수정", description = "상품 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.")
101+
@PatchMapping("/products/{productId}")
102+
public DataResponse<ProductSimpleResponse> updateProduct(
103+
@PathVariable Long productId,
104+
@RequestBody AdminProductUpdateRequest request) {
105+
ProductSimpleResponse response = adminService.updateProduct(productId, request);
106+
return DataResponse.from(response);
107+
}
108+
109+
@Operation(summary = "상품 옵션 수정", description = "상품 옵션 정보를 수정합니다. 수정할 필드만 입력하면 됩니다.")
110+
@PatchMapping("/product-options/{optionId}")
111+
public DataResponse<ProductOptionResponse> updateProductOption(
112+
@PathVariable Long optionId,
113+
@RequestBody AdminProductOptionUpdateRequest request) {
114+
ProductOptionResponse response = adminService.updateProductOption(optionId, request);
115+
return DataResponse.from(response);
116+
}
117+
118+
@Operation(summary = "상품 옵션 삭제", description = "상품 옵션을 삭제합니다.")
119+
@DeleteMapping("/product-options/{optionId}")
120+
public DataResponse<String> deleteProductOption(@PathVariable Long optionId) {
121+
adminService.deleteProductOption(optionId);
122+
return DataResponse.from("상품 옵션이 삭제되었습니다.");
123+
}
60124
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.ongil.backend.domain.admin.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
@Getter
8+
@NoArgsConstructor
9+
@Schema(description = "브랜드 수정 요청")
10+
public class AdminBrandUpdateRequest {
11+
12+
@Schema(description = "브랜드명", example = "나이키")
13+
private String name;
14+
15+
@Schema(description = "브랜드 설명", example = "스포츠 의류 브랜드")
16+
private String description;
17+
18+
@Schema(description = "로고 이미지 URL", example = "https://example.com/logo.png")
19+
private String logoImageUrl;
20+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.ongil.backend.domain.admin.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
@Getter
8+
@NoArgsConstructor
9+
@Schema(description = "카테고리 수정 요청")
10+
public class AdminCategoryUpdateRequest {
11+
12+
@Schema(description = "카테고리명", example = "상의")
13+
private String name;
14+
15+
@Schema(description = "아이콘 URL", example = "https://example.com/icon.png")
16+
private String iconUrl;
17+
18+
@Schema(description = "정렬 순서", example = "1")
19+
private Integer displayOrder;
20+
21+
@Schema(description = "상위 카테고리 ID (하위 카테고리인 경우)", example = "1")
22+
private Long parentCategoryId;
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.ongil.backend.domain.admin.dto.request;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
@Getter
8+
@NoArgsConstructor
9+
@Schema(description = "상품 옵션 수정 요청")
10+
public class AdminProductOptionUpdateRequest {
11+
12+
@Schema(description = "사이즈", example = "M")
13+
private String size;
14+
15+
@Schema(description = "색상", example = "화이트")
16+
private String color;
17+
18+
@Schema(description = "재고 수량", example = "100")
19+
private Integer stock;
20+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.ongil.backend.domain.admin.dto.request;
2+
3+
import com.ongil.backend.domain.product.enums.ProductType;
4+
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Getter
10+
@NoArgsConstructor
11+
@Schema(description = "상품 수정 요청")
12+
public class AdminProductUpdateRequest {
13+
14+
@Schema(description = "상품명", example = "베이직 코튼 티셔츠")
15+
private String name;
16+
17+
@Schema(description = "상품 설명", example = "편안한 착용감의 코튼 티셔츠입니다.")
18+
private String description;
19+
20+
@Schema(description = "가격", example = "29000")
21+
private Integer price;
22+
23+
@Schema(description = "소재 정보", example = "면 100%")
24+
private String materialOriginal;
25+
26+
@Schema(description = "이미지 URL들 (쉼표로 구분)", example = "https://example.com/img1.jpg,https://example.com/img2.jpg")
27+
private String imageUrls;
28+
29+
@Schema(description = "사이즈들 (쉼표로 구분)", example = "S,M,L,XL")
30+
private String sizes;
31+
32+
@Schema(description = "색상들 (쉼표로 구분)", example = "화이트,블랙,네이비")
33+
private String colors;
34+
35+
@Schema(description = "할인율 (%)", example = "10")
36+
private Integer discountRate;
37+
38+
@Schema(description = "상품 타입", example = "NORMAL")
39+
private ProductType productType;
40+
41+
@Schema(description = "브랜드 ID", example = "1")
42+
private Long brandId;
43+
44+
@Schema(description = "카테고리 ID", example = "1")
45+
private Long categoryId;
46+
}

src/main/java/com/ongil/backend/domain/admin/service/AdminService.java

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,13 @@
44
import org.springframework.transaction.annotation.Transactional;
55

66
import com.ongil.backend.domain.admin.dto.request.AdminBrandCreateRequest;
7+
import com.ongil.backend.domain.admin.dto.request.AdminBrandUpdateRequest;
78
import com.ongil.backend.domain.admin.dto.request.AdminCategoryCreateRequest;
9+
import com.ongil.backend.domain.admin.dto.request.AdminCategoryUpdateRequest;
810
import com.ongil.backend.domain.admin.dto.request.AdminProductCreateRequest;
911
import com.ongil.backend.domain.admin.dto.request.AdminProductOptionCreateRequest;
12+
import com.ongil.backend.domain.admin.dto.request.AdminProductOptionUpdateRequest;
13+
import com.ongil.backend.domain.admin.dto.request.AdminProductUpdateRequest;
1014
import com.ongil.backend.domain.brand.converter.BrandConverter;
1115
import com.ongil.backend.domain.brand.dto.response.BrandResponse;
1216
import com.ongil.backend.domain.brand.entity.Brand;
@@ -127,4 +131,137 @@ public ProductOptionResponse createProductOption(AdminProductOptionCreateRequest
127131
.stockStatus(savedOption.getStockStatus())
128132
.build();
129133
}
134+
135+
// 브랜드 수정
136+
public BrandResponse updateBrand(Long brandId, AdminBrandUpdateRequest request) {
137+
Brand brand = brandRepository.findById(brandId)
138+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.BRAND_NOT_FOUND));
139+
140+
brand.updateBrand(
141+
request.getName(),
142+
request.getDescription(),
143+
request.getLogoImageUrl()
144+
);
145+
146+
return brandConverter.toResponse(brand);
147+
}
148+
149+
// 브랜드 삭제
150+
public void deleteBrand(Long brandId) {
151+
Brand brand = brandRepository.findById(brandId)
152+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.BRAND_NOT_FOUND));
153+
154+
// 해당 브랜드를 사용하는 상품이 있는지 확인
155+
if (productRepository.existsByBrandId(brandId)) {
156+
throw new ValidationException(ErrorCode.CANNOT_DELETE_BRAND_WITH_PRODUCTS);
157+
}
158+
159+
brandRepository.delete(brand);
160+
}
161+
162+
// 카테고리 수정
163+
public CategorySimpleResponse updateCategory(Long categoryId, AdminCategoryUpdateRequest request) {
164+
Category category = categoryRepository.findById(categoryId)
165+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.CATEGORY_NOT_FOUND));
166+
167+
Category parentCategory = null;
168+
if (request.getParentCategoryId() != null) {
169+
parentCategory = categoryRepository.findById(request.getParentCategoryId())
170+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.CATEGORY_NOT_FOUND));
171+
}
172+
173+
category.updateCategory(
174+
request.getName(),
175+
request.getIconUrl(),
176+
request.getDisplayOrder(),
177+
parentCategory
178+
);
179+
180+
return categoryConverter.toSimpleResponse(category);
181+
}
182+
183+
// 카테고리 삭제
184+
public void deleteCategory(Long categoryId) {
185+
Category category = categoryRepository.findById(categoryId)
186+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.CATEGORY_NOT_FOUND));
187+
188+
// 해당 카테고리를 사용하는 상품이 있는지 확인
189+
if (productRepository.existsByCategoryId(categoryId)) {
190+
throw new ValidationException(ErrorCode.CANNOT_DELETE_CATEGORY_WITH_PRODUCTS);
191+
}
192+
193+
// 하위 카테고리가 있는지 확인
194+
if (categoryRepository.existsByParentCategoryId(categoryId)) {
195+
throw new ValidationException(ErrorCode.CANNOT_DELETE_CATEGORY_WITH_SUBCATEGORIES);
196+
}
197+
198+
categoryRepository.delete(category);
199+
}
200+
201+
// 상품 수정
202+
public ProductSimpleResponse updateProduct(Long productId, AdminProductUpdateRequest request) {
203+
Product product = productRepository.findById(productId)
204+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.PRODUCT_NOT_FOUND));
205+
206+
Brand brand = null;
207+
if (request.getBrandId() != null) {
208+
brand = brandRepository.findById(request.getBrandId())
209+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.BRAND_NOT_FOUND));
210+
}
211+
212+
Category category = null;
213+
if (request.getCategoryId() != null) {
214+
category = categoryRepository.findById(request.getCategoryId())
215+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.CATEGORY_NOT_FOUND));
216+
217+
// 상품은 하위 카테고리에만 등록 가능
218+
if (category.getParentCategory() == null) {
219+
throw new ValidationException(ErrorCode.INVALID_CATEGORY);
220+
}
221+
}
222+
223+
product.updateProduct(
224+
request.getName(),
225+
request.getDescription(),
226+
request.getPrice(),
227+
request.getMaterialOriginal(),
228+
request.getImageUrls(),
229+
request.getSizes(),
230+
request.getColors(),
231+
request.getDiscountRate(),
232+
request.getProductType(),
233+
brand,
234+
category
235+
);
236+
237+
return productConverter.toSimpleResponse(product);
238+
}
239+
240+
// 상품 옵션 수정
241+
public ProductOptionResponse updateProductOption(Long optionId, AdminProductOptionUpdateRequest request) {
242+
ProductOption productOption = productOptionRepository.findById(optionId)
243+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.PRODUCT_OPTION_NOT_FOUND));
244+
245+
productOption.updateProductOption(
246+
request.getSize(),
247+
request.getColor(),
248+
request.getStock()
249+
);
250+
251+
return ProductOptionResponse.builder()
252+
.optionId(productOption.getId())
253+
.size(productOption.getSize())
254+
.color(productOption.getColor())
255+
.stock(productOption.getStock())
256+
.stockStatus(productOption.getStockStatus())
257+
.build();
258+
}
259+
260+
// 상품 옵션 삭제
261+
public void deleteProductOption(Long optionId) {
262+
ProductOption productOption = productOptionRepository.findById(optionId)
263+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.PRODUCT_OPTION_NOT_FOUND));
264+
265+
productOptionRepository.delete(productOption);
266+
}
130267
}

src/main/java/com/ongil/backend/domain/brand/entity/Brand.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,17 @@ public Brand(String name, String description, String logoImageUrl) {
3434
this.description = description;
3535
this.logoImageUrl = logoImageUrl;
3636
}
37+
38+
// 브랜드 정보 수정
39+
public void updateBrand(String name, String description, String logoImageUrl) {
40+
if (name != null) {
41+
this.name = name;
42+
}
43+
if (description != null) {
44+
this.description = description;
45+
}
46+
if (logoImageUrl != null) {
47+
this.logoImageUrl = logoImageUrl;
48+
}
49+
}
3750
}

src/main/java/com/ongil/backend/domain/category/entity/Category.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,26 @@ public Category(String name, String iconUrl, Integer displayOrder, Category pare
4545
this.displayOrder = displayOrder;
4646
this.parentCategory = parentCategory;
4747
}
48+
49+
// 카테고리 정보 수정
50+
public void updateCategory(String name, String iconUrl, Integer displayOrder, Category parentCategory) {
51+
if (name != null) {
52+
this.name = name;
53+
}
54+
if (iconUrl != null) {
55+
this.iconUrl = iconUrl;
56+
}
57+
if (displayOrder != null) {
58+
this.displayOrder = displayOrder;
59+
}
60+
if (parentCategory != null) {
61+
// 자기 참조(순환 참조) 방지
62+
if (parentCategory.getId().equals(this.id)) {
63+
throw new com.ongil.backend.global.common.exception.ValidationException(
64+
com.ongil.backend.global.common.exception.ErrorCode.CATEGORY_SELF_REFERENCE
65+
);
66+
}
67+
this.parentCategory = parentCategory;
68+
}
69+
}
4870
}

0 commit comments

Comments
 (0)