Skip to content

Commit

Permalink
Merge pull request #29 from taco-official/KL-70/상품-삭제-api-구현
Browse files Browse the repository at this point in the history
feat(KL-70): delete Product API
  • Loading branch information
ohhamma authored Aug 1, 2024
2 parents 05b6ece + e8ce07a commit c6524fe
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down Expand Up @@ -29,7 +30,9 @@ public class ProductController {

@GetMapping("/{id}")
@Operation(summary = "상품 상세 조회", description = "상품 상세 정보를 조회합니다.")
public ResponseEntity<ProductDetailResponseDto> getProductInfoById(@PathVariable Long id) {
public ResponseEntity<ProductDetailResponseDto> getProductInfoById(
@PathVariable Long id
) {
ProductDetailResponseDto productDto = productService.getProductInfoById(id);
return ResponseEntity.ok().body(productDto);
}
Expand All @@ -52,4 +55,13 @@ public ResponseEntity<ProductDetailResponseDto> updateProduct(
ProductDetailResponseDto productDto = productService.updateProduct(id, updateRequest);
return ResponseEntity.ok().body(productDto);
}

@DeleteMapping("/{id}")
@Operation(summary = "상품 삭제", description = "상품을 삭제합니다.")
public ResponseEntity<ProductDetailResponseDto> deleteProduct(
@PathVariable Long id
) {
productService.deleteProduct(id);
return ResponseEntity.noContent().build();
}
}
5 changes: 4 additions & 1 deletion src/main/java/taco/klkl/domain/product/domain/Product.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ public class Product {
)
private LocalDateTime createdAt;

@Column(name = "price")
@Column(
name = "price",
nullable = false
)
@ColumnDefault(DefaultConstants.DEFAULT_INT_STRING)
private Integer price;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ public record ProductCreateRequestDto(
@Size(max = ProductConstants.DESCRIPTION_MAX_LENGTH, message = ProductValidationMessages.DESCRIPTION_SIZE)
String description,

@NotNull(message = ProductValidationMessages.ADDRESS_NOT_NULL)
@Size(max = ProductConstants.ADDRESS_MAX_LENGTH, message = ProductValidationMessages.ADDRESS_SIZE)
String address,

@NotNull(message = ProductValidationMessages.PRICE_NOT_NULL)
@PositiveOrZero(message = ProductValidationMessages.PRICE_POSITIVE_OR_ZERO)
Integer price,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import taco.klkl.domain.product.dto.response.ProductDetailResponseDto;
import taco.klkl.domain.product.exception.ProductNotFoundException;
import taco.klkl.domain.user.domain.User;
import taco.klkl.global.common.constants.ProductConstants;
import taco.klkl.global.util.UserUtil;

@Service
Expand Down Expand Up @@ -43,6 +42,13 @@ public ProductDetailResponseDto updateProduct(final Long id, final ProductUpdate
return ProductDetailResponseDto.from(product);
}

@Transactional
public void deleteProduct(final Long id) {
final Product product = productRepository.findById(id)
.orElseThrow(ProductNotFoundException::new);
productRepository.delete(product);
}

private Product createProductEntity(final ProductCreateRequestDto productDto) {
final User user = userUtil.findTestUser();
return Product.of(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package taco.klkl.global.common.constants;

import taco.klkl.domain.product.domain.Product;

public final class ProductConstants {

public static final int DEFAULT_PRICE = 0;
Expand All @@ -10,6 +12,17 @@ public final class ProductConstants {
public static final int DESCRIPTION_MAX_LENGTH = 2000;
public static final int ADDRESS_MAX_LENGTH = 100;

public static final Product TEST_PRODUCT = Product.of(
UserConstants.TEST_USER,
"testProduct",
"testDescription",
"testAddress",
1000,
1L,
2L,
3L
);

private ProductConstants() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ public final class ProductValidationMessages {
public static final String DESCRIPTION_NOT_BLANK = "상품 설명은 비어있을 수 없습니다.";
public static final String DESCRIPTION_SIZE = "상품 설명은 2000자 이하여야 합니다.";

public static final String ADDRESS_NOT_NULL = "주소는 필수 항목입니다.";
public static final String ADDRESS_SIZE = "주소는 100자 이하여야 합니다.";

public static final String PRICE_NOT_NULL = "가격은 필수 항목입니다.";
public static final String PRICE_POSITIVE_OR_ZERO = "가격은 0 이상이어야 합니다.";

public static final String CITY_ID_NOT_NULL = "도시 ID는 필수 항목입니다.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import taco.klkl.domain.product.dto.request.ProductCreateRequestDto;
import taco.klkl.domain.product.dto.request.ProductUpdateRequestDto;
import taco.klkl.domain.product.dto.response.ProductDetailResponseDto;
import taco.klkl.domain.product.exception.ProductNotFoundException;
import taco.klkl.domain.product.service.ProductService;
import taco.klkl.domain.user.domain.User;
import taco.klkl.global.error.exception.ErrorCode;
Expand Down Expand Up @@ -190,4 +191,45 @@ public void testUpdateProduct() throws Exception {
.andExpect(jsonPath("$.data.currencyId", is(productDetailResponseDto.currencyId().intValue())))
.andExpect(jsonPath("$.timestamp", notNullValue()));
}

@Test
@DisplayName("상품 삭제 API 테스트")
public void testDeleteProduct() throws Exception {
// given
Long productId = 1L;
doNothing().when(productService).deleteProduct(productId);

// when & then
mockMvc.perform(delete("/v1/products/{id}", productId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNoContent())
.andExpect(jsonPath("$.isSuccess", is(true)))
.andExpect(jsonPath("$.code", is("C000")))
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.timestamp", notNullValue()));

verify(productService, times(1)).deleteProduct(productId);
}

@Test
@DisplayName("존재하지 않는 상품 삭제 시 예외 처리 테스트")
public void testDeleteNonExistentProduct() throws Exception {
// given
Long nonExistentProductId = 999L;
doThrow(new ProductNotFoundException())
.when(productService).deleteProduct(nonExistentProductId);

// when & then
ErrorCode productNotFoundError = ErrorCode.PRODUCT_NOT_FOUND;
mockMvc.perform(delete("/v1/products/{id}", nonExistentProductId)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.isSuccess", is(false)))
.andExpect(jsonPath("$.code", is(productNotFoundError.getCode())))
.andExpect(jsonPath("$.data.code", is(productNotFoundError.getCode())))
.andExpect(jsonPath("$.data.message", is(productNotFoundError.getMessage())))
.andExpect(jsonPath("$.timestamp", notNullValue()));

verify(productService, times(1)).deleteProduct(nonExistentProductId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import taco.klkl.global.common.constants.ProductConstants;
import taco.klkl.global.common.constants.ProductValidationMessages;

class ProductCreateRequestDtoTest {
Expand All @@ -28,7 +29,7 @@ void setUp() {

@Test
@DisplayName("유효한 ProductCreateRequestDto 생성 시 검증 통과")
void validProductCreateRequestDto() {
void testValidProductCreateRequestDto() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"Valid product description",
Expand All @@ -45,7 +46,7 @@ void validProductCreateRequestDto() {

@Test
@DisplayName("상품명이 null일 때 검증 실패")
void nullProductName() {
void testNullProductName() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
null,
"Valid product description",
Expand All @@ -66,7 +67,7 @@ void nullProductName() {

@Test
@DisplayName("상품명이 빈 문자열일 때 검증 실패")
void emptyProductName() {
void testEmptyProductName() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"",
"Valid product description",
Expand All @@ -88,7 +89,7 @@ void emptyProductName() {
@ParameterizedTest
@ValueSource(ints = {101, 200, 1000})
@DisplayName("상품명이 최대 길이를 초과할 때 검증 실패")
void productNameExceedsMaxLength(int nameLength) {
void testProductNameOverMaxLength(int nameLength) {
String longName = "a".repeat(nameLength);
ProductCreateRequestDto dto = new ProductCreateRequestDto(
longName,
Expand All @@ -107,7 +108,7 @@ void productNameExceedsMaxLength(int nameLength) {

@Test
@DisplayName("상품 설명이 null일 때 검증 실패")
void nullProductDescription() {
void testNullProductDescription() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
null,
Expand All @@ -128,7 +129,7 @@ void nullProductDescription() {

@Test
@DisplayName("상품 설명이 빈 문자열일 때 검증 실패")
void emptyProductDescription() {
void testEmptyProductDescription() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"",
Expand All @@ -150,7 +151,7 @@ void emptyProductDescription() {
@ParameterizedTest
@ValueSource(ints = {2001, 3000, 5000})
@DisplayName("상품 설명이 최대 길이를 초과할 때 검증 실패")
void productDescriptionExceedsMaxLength(int descriptionLength) {
void testProductDescriptionOverMaxLength(int descriptionLength) {
String longDescription = "a".repeat(descriptionLength);
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
Expand All @@ -167,10 +168,67 @@ void productDescriptionExceedsMaxLength(int descriptionLength) {
assertEquals(ProductValidationMessages.DESCRIPTION_SIZE, violations.iterator().next().getMessage());
}

@Test
@DisplayName("주소가 null일 때 검증 실패")
void testNullAddress() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"Valid product description",
null,
100,
1L,
2L,
3L
);

Set<ConstraintViolation<ProductCreateRequestDto>> violations = validator.validate(dto);
assertFalse(violations.isEmpty());

boolean foundNotNullMessage = violations.stream()
.anyMatch(violation -> violation.getMessage().equals(ProductValidationMessages.ADDRESS_NOT_NULL));
assertTrue(foundNotNullMessage, "Expected ADDRESS_NOT_NULL message not found");
}

@Test
@DisplayName("주소가 빈 문자열일 때 검증 통과")
void testEmptyAddress() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"Valid product description",
"",
100,
1L,
2L,
3L
);

Set<ConstraintViolation<ProductCreateRequestDto>> violations = validator.validate(dto);
assertTrue(violations.isEmpty());
}

@ParameterizedTest
@ValueSource(ints = {0, 1, ProductConstants.ADDRESS_MAX_LENGTH})
@DisplayName("주소가 최대 길이 이하일 때 검증 통과")
void testAddressUnderMaxLength(int addressLength) {
String address = "a".repeat(addressLength);
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"Valid product description",
address,
100,
1L,
2L,
3L
);

Set<ConstraintViolation<ProductCreateRequestDto>> violations = validator.validate(dto);
assertTrue(violations.isEmpty());
}

@ParameterizedTest
@ValueSource(ints = {101, 200, 500})
@DisplayName("주소가 최대 길이를 초과할 때 검증 실패")
void addressExceedsMaxLength(int addressLength) {
void testAddressOverMaxLength(int addressLength) {
String longAddress = "a".repeat(addressLength);
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
Expand All @@ -187,10 +245,49 @@ void addressExceedsMaxLength(int addressLength) {
assertEquals(ProductValidationMessages.ADDRESS_SIZE, violations.iterator().next().getMessage());
}

@Test
@DisplayName("가격이 null일 때 검증 실패")
void testNullPrice() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"Valid product description",
"Valid address",
null,
1L,
2L,
3L
);

Set<ConstraintViolation<ProductCreateRequestDto>> violations = validator.validate(dto);
assertFalse(violations.isEmpty());

boolean foundNotNullMessage = violations.stream()
.anyMatch(violation -> violation.getMessage().equals(ProductValidationMessages.PRICE_NOT_NULL));
assertTrue(foundNotNullMessage, "Expected PRICE_NOT_NULL message not found");
}

@ParameterizedTest
@ValueSource(ints = {0, 100, 1000})
@DisplayName("가격이 0 이상일 때 검증 통과")
void testZeroOrPositivePrice() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"Valid product description",
"Valid address",
0,
1L,
2L,
3L
);

Set<ConstraintViolation<ProductCreateRequestDto>> violations = validator.validate(dto);
assertTrue(violations.isEmpty());
}

@ParameterizedTest
@ValueSource(ints = {-1, -100, -1000})
@DisplayName("가격이 음수일 때 검증 실패")
void negativePrice(int price) {
void testNegativePrice(int price) {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"Valid product description",
Expand All @@ -208,7 +305,7 @@ void negativePrice(int price) {

@Test
@DisplayName("도시 ID가 null일 때 검증 실패")
void nullCityId() {
void testNullCityId() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"Valid product description",
Expand All @@ -226,7 +323,7 @@ void nullCityId() {

@Test
@DisplayName("상품 소분류 ID가 null일 때 검증 실패")
void nullSubcategoryId() {
void testNullSubcategoryId() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"Valid product description",
Expand All @@ -244,7 +341,7 @@ void nullSubcategoryId() {

@Test
@DisplayName("통화 ID가 null일 때 검증 실패")
void nullCurrencyId() {
void testNullCurrencyId() {
ProductCreateRequestDto dto = new ProductCreateRequestDto(
"Valid Product Name",
"Valid product description",
Expand Down
Loading

0 comments on commit c6524fe

Please sign in to comment.