diff --git a/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/controller/AdminProductController.kt b/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/controller/AdminProductController.kt index 0caed7cc..33c435c8 100644 --- a/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/controller/AdminProductController.kt +++ b/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/controller/AdminProductController.kt @@ -82,7 +82,11 @@ class AdminProductController( ) } - @Operation(summary = "상품 판매 여부 수정", description = "상품의 판매 여부를 수정합니다.") + @Operation( + summary = "상품 판매 여부 수정", + description = "상품의 판매 여부를 수정합니다. deprecated: 상품 상태 수정 API로 대체됩니다.", + deprecated = true + ) @PutMapping("/product/sellable") fun updateProductSellable( @AdminUsername username: String, @@ -96,6 +100,20 @@ class AdminProductController( ) } + @Operation(summary = "상품 상태 수정", description = "상품의 상태를 수정합니다.") + @PutMapping("/product/status") + fun updateProductStatus( + @AdminUsername username: String, + @RequestBody body: UpdateProductStatusRequestBody, + ): ProductDto { + return productFacade.updateProductStatus( + username, + body.workspaceId, + body.productId, + body.status + ) + } + @Operation(summary = "상품 삭제", description = "상품을 삭제합니다.") @DeleteMapping("/product") fun deleteProduct( diff --git a/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/dto/common/ProductDto.kt b/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/dto/common/ProductDto.kt index 6654c5bd..3a2b0f27 100644 --- a/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/dto/common/ProductDto.kt +++ b/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/dto/common/ProductDto.kt @@ -1,7 +1,7 @@ package com.kioschool.kioschoolapi.domain.product.dto.common -import com.kioschool.kioschoolapi.domain.product.dto.common.ProductCategoryDto import com.kioschool.kioschoolapi.domain.product.entity.Product +import com.kioschool.kioschoolapi.global.common.enums.ProductStatus import java.time.LocalDateTime data class ProductDto( @@ -11,6 +11,7 @@ data class ProductDto( val price: Int, val imageUrl: String?, val isSellable: Boolean?, + val status: ProductStatus?, val productCategory: ProductCategoryDto?, val createdAt: LocalDateTime?, val updatedAt: LocalDateTime? @@ -24,6 +25,7 @@ data class ProductDto( price = product.price, imageUrl = product.imageUrl, isSellable = product.isSellable, + status = product.status, productCategory = product.productCategory?.let { ProductCategoryDto.of(it) }, createdAt = product.createdAt, updatedAt = product.updatedAt diff --git a/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/dto/request/UpdateProductStatusRequestBody.kt b/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/dto/request/UpdateProductStatusRequestBody.kt new file mode 100644 index 00000000..2fbeb0a9 --- /dev/null +++ b/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/dto/request/UpdateProductStatusRequestBody.kt @@ -0,0 +1,9 @@ +package com.kioschool.kioschoolapi.domain.product.dto.request + +import com.kioschool.kioschoolapi.global.common.enums.ProductStatus + +class UpdateProductStatusRequestBody( + val workspaceId: Long, + val productId: Long, + val status: ProductStatus +) \ No newline at end of file diff --git a/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/entity/Product.kt b/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/entity/Product.kt index aed45c76..a615cbce 100644 --- a/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/entity/Product.kt +++ b/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/entity/Product.kt @@ -3,6 +3,7 @@ package com.kioschool.kioschoolapi.domain.product.entity import com.fasterxml.jackson.annotation.JsonIgnore import com.kioschool.kioschoolapi.domain.workspace.entity.Workspace import com.kioschool.kioschoolapi.global.common.entity.BaseEntity +import com.kioschool.kioschoolapi.global.common.enums.ProductStatus import jakarta.persistence.Entity import jakarta.persistence.ManyToOne import jakarta.persistence.Table @@ -15,6 +16,7 @@ class Product( var price: Int, var imageUrl: String? = null, var isSellable: Boolean? = true, + var status: ProductStatus = ProductStatus.SELLING, @ManyToOne @JsonIgnore val workspace: Workspace, diff --git a/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/facade/ProductFacade.kt b/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/facade/ProductFacade.kt index 175147a1..1409e3cf 100644 --- a/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/facade/ProductFacade.kt +++ b/src/main/kotlin/com/kioschool/kioschoolapi/domain/product/facade/ProductFacade.kt @@ -8,6 +8,7 @@ import com.kioschool.kioschoolapi.domain.workspace.exception.WorkspaceInaccessib import com.kioschool.kioschoolapi.domain.workspace.service.WorkspaceService import com.kioschool.kioschoolapi.global.aws.S3Service import com.kioschool.kioschoolapi.global.cache.constant.CacheNames +import com.kioschool.kioschoolapi.global.common.enums.ProductStatus import org.springframework.cache.annotation.Cacheable import org.springframework.stereotype.Component import org.springframework.web.multipart.MultipartFile @@ -167,4 +168,17 @@ class ProductFacade( return productService.saveProductCategories(productCategories) .map { ProductCategoryDto.of(it) } } + + fun updateProductStatus( + username: String, + workspaceId: Long, + productId: Long, + status: ProductStatus + ): ProductDto { + val product = productService.getProduct(productId) + workspaceService.checkAccessible(username, workspaceId) + + product.status = status + return ProductDto.of(productService.saveProduct(product)) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/kioschool/kioschoolapi/global/common/enums/ProductStatus.kt b/src/main/kotlin/com/kioschool/kioschoolapi/global/common/enums/ProductStatus.kt new file mode 100644 index 00000000..a59dadb2 --- /dev/null +++ b/src/main/kotlin/com/kioschool/kioschoolapi/global/common/enums/ProductStatus.kt @@ -0,0 +1,7 @@ +package com.kioschool.kioschoolapi.global.common.enums + +enum class ProductStatus { + SELLING, + SOLD_OUT, + HIDDEN, +} \ No newline at end of file diff --git a/src/main/kotlin/com/kioschool/kioschoolapi/global/schedule/script/InitProductStatus.kt b/src/main/kotlin/com/kioschool/kioschoolapi/global/schedule/script/InitProductStatus.kt new file mode 100644 index 00000000..e54e5060 --- /dev/null +++ b/src/main/kotlin/com/kioschool/kioschoolapi/global/schedule/script/InitProductStatus.kt @@ -0,0 +1,25 @@ +package com.kioschool.kioschoolapi.global.schedule.script + +import com.kioschool.kioschoolapi.domain.product.repository.ProductRepository +import com.kioschool.kioschoolapi.global.common.enums.ProductStatus +import com.kioschool.kioschoolapi.global.schedule.Runnable +import org.springframework.stereotype.Component + +@Component +class InitProductStatus( + private val productRepository: ProductRepository +) : Runnable { + override fun run() { + val products = productRepository.findAll() + products.filter { + it.status == null + }.forEach { + it.status = if (it.isSellable == true) { + ProductStatus.SELLING + } else { + ProductStatus.HIDDEN + } + } + productRepository.saveAll(products) + } +} \ No newline at end of file diff --git a/src/main/resources/db/changelog/2025/11/22-01-changelog.xml b/src/main/resources/db/changelog/2025/11/22-01-changelog.xml new file mode 100644 index 00000000..6f1f3920 --- /dev/null +++ b/src/main/resources/db/changelog/2025/11/22-01-changelog.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/test/kotlin/com/kioschool/kioschoolapi/product/facade/ProductFacadeTest.kt b/src/test/kotlin/com/kioschool/kioschoolapi/product/facade/ProductFacadeTest.kt index e7b4ba77..6035701f 100644 --- a/src/test/kotlin/com/kioschool/kioschoolapi/product/facade/ProductFacadeTest.kt +++ b/src/test/kotlin/com/kioschool/kioschoolapi/product/facade/ProductFacadeTest.kt @@ -8,6 +8,7 @@ import com.kioschool.kioschoolapi.domain.workspace.exception.WorkspaceInaccessib import com.kioschool.kioschoolapi.domain.workspace.service.WorkspaceService import com.kioschool.kioschoolapi.factory.SampleEntity import com.kioschool.kioschoolapi.global.aws.S3Service +import com.kioschool.kioschoolapi.global.common.enums.ProductStatus import io.kotest.core.spec.style.DescribeSpec import io.mockk.* import org.junit.jupiter.api.assertThrows @@ -680,4 +681,49 @@ class ProductFacadeTest : DescribeSpec({ verify(exactly = 0) { productService.saveProductCategories(any()) } } } + + describe("updateProductStatus") { + it("should update product status") { + val username = "username" + val workspaceId = 1L + val productId = 1L + val status = ProductStatus.SOLD_OUT + val product = SampleEntity.product + every { productService.getProduct(productId) } returns product + every { workspaceService.checkAccessible(username, workspaceId) } just Runs + every { productService.saveProduct(product) } returns product + + val result = sut.updateProductStatus(username, workspaceId, productId, status) + + assert(result.id == product.id) + assert(result.status == status) + + verify { productService.getProduct(productId) } + verify { workspaceService.checkAccessible(username, workspaceId) } + verify { productService.saveProduct(product) } + } + + it("should throw WorkspaceInaccessibleException") { + val username = "username" + val workspaceId = 1L + val productId = 1L + val status = ProductStatus.SOLD_OUT + val product = SampleEntity.product + every { productService.getProduct(productId) } returns product + every { + workspaceService.checkAccessible( + username, + workspaceId + ) + } throws WorkspaceInaccessibleException() + + assertThrows { + sut.updateProductStatus(username, workspaceId, productId, status) + } + + verify { productService.getProduct(productId) } + verify { workspaceService.checkAccessible(username, workspaceId) } + verify(exactly = 0) { productService.saveProduct(any()) } + } + } }) \ No newline at end of file