diff --git a/src/main/kotlin/org/store/clothstar/category/controller/CategoryController.kt b/src/main/kotlin/org/store/clothstar/category/controller/CategoryController.kt index 9a4d2f5..7b7adeb 100644 --- a/src/main/kotlin/org/store/clothstar/category/controller/CategoryController.kt +++ b/src/main/kotlin/org/store/clothstar/category/controller/CategoryController.kt @@ -1,6 +1,10 @@ package org.store.clothstar.category.controller import io.swagger.v3.oas.annotations.Operation +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice +import org.springframework.data.web.PageableDefault import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated @@ -11,14 +15,15 @@ import org.store.clothstar.category.dto.response.CategoryResponse import org.store.clothstar.category.service.CategoryService import org.store.clothstar.common.dto.MessageDTO import org.store.clothstar.common.util.URIBuilder +import org.store.clothstar.product.dto.response.ProductListResponse +import org.store.clothstar.product.service.ProductService @RestController @RequestMapping("/v1/categories") class CategoryController( private val categoryService: CategoryService, -// private val productLineService: ProductLineService + private val productService: ProductService ) { - @Operation(summary = "전체 카테고리 조회", description = "모든 카테고리를 조회한다.") @GetMapping fun getAllCategories(): ResponseEntity> { @@ -26,6 +31,37 @@ class CategoryController( return ResponseEntity.ok(categoryResponses) } + + @Operation( + summary = "카테고리별 상품 조회 (Offset Paging)", + description = "카테고리 ID로 해당 카테고리에 속하는 모든 상품을 Offset Paging을 통해 조회한다." + ) + @GetMapping("/{categoryId}/products/offset") + fun getProductLinesByCategory( + @PathVariable categoryId: Long, + @PageableDefault(size = 18) pageable: Pageable, + @RequestParam(required = false) keyword: String? + ): ResponseEntity> { + val productResponses: Page = + productService.getProductLinesByCategoryWithOffsetPaging(categoryId, pageable, keyword) + return ResponseEntity.ok().body>(productResponses) + } + + @Operation( + summary = "카테고리별 상품 조회 (Slice Paging)", + description = "카테고리 ID로 해당 카테고리에 속하는 모든 상품을 Slice Paging을 통해 조회한다." + ) + @GetMapping("/{categoryId}/products/slice") + fun getProductLinesByCategorySlice( + @PathVariable categoryId: Long, + @PageableDefault(size = 18) pageable: Pageable, + @RequestParam(required = false) keyword: String? + ): ResponseEntity> { + val productResponses: Slice = + productService.getProductLinesByCategoryWithSlicePaging(categoryId, pageable, keyword) + return ResponseEntity.ok().body>(productResponses) + } + @Operation(summary = "카테고리 상세 조회", description = "id로 카테고리 한개를 상세 조회한다.") @GetMapping("/{categoryId}") fun getCategory(@PathVariable categoryId: Long): ResponseEntity { diff --git a/src/test/kotlin/org/store/clothstar/common/config/JpaQueryFactoryConfig.kt b/src/main/kotlin/org/store/clothstar/common/config/JpaQueryFactoryConfig.kt similarity index 55% rename from src/test/kotlin/org/store/clothstar/common/config/JpaQueryFactoryConfig.kt rename to src/main/kotlin/org/store/clothstar/common/config/JpaQueryFactoryConfig.kt index 6922faa..ed7038f 100644 --- a/src/test/kotlin/org/store/clothstar/common/config/JpaQueryFactoryConfig.kt +++ b/src/main/kotlin/org/store/clothstar/common/config/JpaQueryFactoryConfig.kt @@ -2,16 +2,15 @@ package org.store.clothstar.common.config import com.querydsl.jpa.impl.JPAQueryFactory import jakarta.persistence.EntityManager -import jakarta.persistence.PersistenceContext import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration -class JpaQueryFactoryConfig { - - @PersistenceContext - private lateinit var entityManager: EntityManager - +class JpaQueryFactoryConfig( + private val em: EntityManager, +) { @Bean - fun jpaQueryFactory() = JPAQueryFactory(entityManager) + fun jpaQueryFactory(): JPAQueryFactory { + return JPAQueryFactory(em) + } } \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/common/config/SecurityConfiguration.kt b/src/main/kotlin/org/store/clothstar/common/config/SecurityConfiguration.kt index ccd6561..fa58217 100644 --- a/src/main/kotlin/org/store/clothstar/common/config/SecurityConfiguration.kt +++ b/src/main/kotlin/org/store/clothstar/common/config/SecurityConfiguration.kt @@ -61,7 +61,7 @@ class SecurityConfiguration( "/v1/members/login", "/signup", "/v1/members/email/**", "/v1/access", "/v1/categories/**", "/v1/products/**", "/v1/productLines/**", "/v2/productLines/**", "/productLinePagingSlice", "/productLinePagingOffset", - "/v3/products/**", + "/v3/products/**", "v3/sellers/products/**", "/v1/orderdetails", "/v1/orders", "membersPagingOffset", "membersPagingSlice", "/v1/orderdetails", "/v1/orders", "/v2/orders", "/v3/orders", "/v1/orders/list", "/v1/orders/list", "/ordersPagingOffset", "/ordersPagingSlice", "/v2/orders/list", diff --git a/src/main/kotlin/org/store/clothstar/common/error/ErrorCode.kt b/src/main/kotlin/org/store/clothstar/common/error/ErrorCode.kt index 7998a21..94475c9 100644 --- a/src/main/kotlin/org/store/clothstar/common/error/ErrorCode.kt +++ b/src/main/kotlin/org/store/clothstar/common/error/ErrorCode.kt @@ -20,6 +20,8 @@ enum class ErrorCode( INVALID_REFRESH_TOKEN(HttpStatus.BAD_REQUEST, "refresh 토큰이 만료되었거나 유효하지 않습니다."), INVALID_AUTH_CERTIFY_NUM(HttpStatus.BAD_REQUEST, "인증번호가 잘못 되었습니다."), + INVALID_ROLE(HttpStatus.BAD_REQUEST, "계정 ID: %d - 이 계정은 요청된 권한(%s)을 가지고 있지 않습니다."), + // Order 관련 에러코드 NOT_FOUND_ORDER(HttpStatus.NOT_FOUND, "존재하지 않는 주문번호입니다."), INVALID_ORDER_STATUS_CONFIRMED(HttpStatus.BAD_REQUEST, "주문이 '입금확인' 상태가 아니므로 요청을 처리할 수 없습니다."), @@ -35,4 +37,9 @@ enum class ErrorCode( this.status = status this.message = message } + + fun withDynamicMessage(vararg args: Any): ErrorCode { + this.message = String.format(this.message, *args) + return this + } } \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/common/error/exception/order/BadRequestException.kt b/src/main/kotlin/org/store/clothstar/common/error/exception/order/BadRequestException.kt index 4d7aebe..56b18d2 100644 --- a/src/main/kotlin/org/store/clothstar/common/error/exception/order/BadRequestException.kt +++ b/src/main/kotlin/org/store/clothstar/common/error/exception/order/BadRequestException.kt @@ -4,8 +4,10 @@ import org.springframework.http.HttpStatus import org.store.clothstar.common.error.ErrorCode class BadRequestException( - val code: ErrorCode + val code: ErrorCode, + vararg args: Any ) : RuntimeException(code.message) { + val httpStatus: HttpStatus get() = code.status diff --git a/src/main/kotlin/org/store/clothstar/member/AccountValidateUtil.kt b/src/main/kotlin/org/store/clothstar/member/AccountValidateUtil.kt new file mode 100644 index 0000000..37a0ae8 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/member/AccountValidateUtil.kt @@ -0,0 +1,19 @@ +package org.store.clothstar.member + +import org.springframework.stereotype.Component +import org.store.clothstar.common.error.ErrorCode +import org.store.clothstar.common.error.exception.order.BadRequestException +import org.store.clothstar.member.domain.Account +import org.store.clothstar.member.domain.MemberRole + +@Component +class AccountValidateUtil ( + +) { + +// fun validateRole(accout: Account, memberRole: MemberRole) { +// if (!accout.role.equals(memberRole)) { +//// throw BadRequestException(ErrorCode.INVALID_ROLE, accout.accountId, memberRole) +// } +// } +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/member/dto/response/SellerSimpleResponse.kt b/src/main/kotlin/org/store/clothstar/member/dto/response/SellerSimpleResponse.kt index c94bc97..00e9011 100644 --- a/src/main/kotlin/org/store/clothstar/member/dto/response/SellerSimpleResponse.kt +++ b/src/main/kotlin/org/store/clothstar/member/dto/response/SellerSimpleResponse.kt @@ -1,7 +1,19 @@ package org.store.clothstar.member.dto.response +import org.store.clothstar.member.domain.Seller + class SellerSimpleResponse( var memberId: Long, val brandName: String, val bizNo: String, -) \ No newline at end of file +) { + companion object { + fun getSellerSimpleResponseBySeller(seller: Seller): SellerSimpleResponse { + return SellerSimpleResponse( + memberId = seller.memberId, + bizNo = seller.bizNo, + brandName = seller.brandName, + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/controller/ProductController.kt b/src/main/kotlin/org/store/clothstar/product/controller/ProductController.kt index 19e0d4c..e654fe4 100644 --- a/src/main/kotlin/org/store/clothstar/product/controller/ProductController.kt +++ b/src/main/kotlin/org/store/clothstar/product/controller/ProductController.kt @@ -2,6 +2,10 @@ package org.store.clothstar.product.controller import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice +import org.springframework.data.web.PageableDefault import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated @@ -9,7 +13,7 @@ import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile import org.store.clothstar.common.dto.MessageDTO import org.store.clothstar.product.dto.request.ProductCreateRequest -import org.store.clothstar.product.dto.request.UpdateDisplayStatusRequest +import org.store.clothstar.product.dto.response.ProductListResponse import org.store.clothstar.product.dto.response.ProductResponse import org.store.clothstar.product.service.ProductApplicationService @@ -19,62 +23,33 @@ import org.store.clothstar.product.service.ProductApplicationService private class ProductController( private val productApplicationService: ProductApplicationService, ) { - @PostMapping - @Operation( - summary = "상품 등록", - description = "카테고리 아이디, 상품 이름, 내용, 가격, 상태를 입력하여 상품을 신규 등록한다." - ) - fun createProduct( - @RequestPart(value = "mainImage", required = false) mainImage: MultipartFile, - @RequestPart(value = "subImages", required = false) subImages: List?, - @RequestPart(value = "dto") @Validated productCreateRequest: ProductCreateRequest - ): ResponseEntity { - // 상품 등록 - productApplicationService.createProduct(mainImage, subImages, productCreateRequest); - val messageDTO = MessageDTO( - HttpStatus.CREATED.value(), - "상품 생성이 정상적으로 처리됐습니다." - ) + // 상품 전체 Offset 페이징 조회 + @GetMapping("/offset") + @Operation(summary = "상품 전체 Offset 페이징 조회", description = "상품 전체 리스트를 Offset 페이징 형식으로 조회한다.") + fun getAllProductsOffsetPaging( + @PageableDefault(size = 18) pageable: Pageable, + @RequestParam(required = false) keyword: String? + ): ResponseEntity> { + val productPages = productApplicationService.getAllProductsOffsetPaging(pageable, keyword) + return ResponseEntity.ok().body(productPages) + } - return ResponseEntity(messageDTO, HttpStatus.CREATED) + // 상품 전체 Slice 페이징 조회 + @GetMapping("/slice") + @Operation(summary = "상품 전체 Slice 페이징 조회", description = "상품 전체 리스트를 Slice 페이징 형식으로 조회한다.") + fun getAllProductsSlicePaging( + @PageableDefault(size = 18) pageable: Pageable, + @RequestParam(required = false) keyword: String? + ): ResponseEntity> { + val productPages = productApplicationService.getAllProductsSlicePaging(pageable, keyword) + return ResponseEntity.ok().body(productPages) } @GetMapping("/{productId}") @Operation(summary = "상품 상세 조회", description = "상품 ID를 사용하여 특정 상품의 상세 정보를 조회한다.") fun getProductDetails(@PathVariable productId: Long): ResponseEntity { - val productResponse = productApplicationService.getProductDetails(productId) + val productResponse = productApplicationService.getProductDetails(productId, false) return ResponseEntity(productResponse, HttpStatus.OK) } - - @PatchMapping("/{productId}/displayStatus") - @Operation(summary = "상품 진열 상태 변경", description = "상품 ID를 사용하여 해당 상품의 진열 상태를 변경합니다.") - fun updateProductDisplayStatus( - @PathVariable productId: Long, - @RequestBody request: UpdateDisplayStatusRequest - ): ResponseEntity { - productApplicationService.updateProductDisplayStatus(productId, request.displayStatus) - - val messageDTO = MessageDTO( - HttpStatus.OK.value(), - "상품 진열 상태가 성공적으로 변경되었습니다." - ) - return ResponseEntity(messageDTO, HttpStatus.OK) - } - - @PatchMapping("/{productId}/items/{itemId}/displayStatus") - @Operation(summary = "아이템 진열 상태 변경", description = "상품 ID와 아이템 ID를 사용하여 해당 아이템의 진열 상태를 변경합니다.") - fun updateItemDisplayStatus( - @PathVariable productId: Long, - @PathVariable itemId: Long, - @RequestBody request: UpdateDisplayStatusRequest - ): ResponseEntity { - productApplicationService.updateItemDisplayStatus(productId, itemId, request.displayStatus) - - val messageDTO = MessageDTO( - HttpStatus.OK.value(), - "아이템 진열 상태가 성공적으로 변경되었습니다." - ) - return ResponseEntity(messageDTO, HttpStatus.OK) - } } \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/controller/ProductSellerController.kt b/src/main/kotlin/org/store/clothstar/product/controller/ProductSellerController.kt new file mode 100644 index 0000000..73b51b6 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/product/controller/ProductSellerController.kt @@ -0,0 +1,138 @@ +package org.store.clothstar.product.controller + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.validation.annotation.Validated +import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile +import org.store.clothstar.common.dto.MessageDTO +import org.store.clothstar.product.dto.request.ProductCreateRequest +import org.store.clothstar.product.dto.request.UpdateDisplayStatusRequest +import org.store.clothstar.product.dto.request.UpdateStockRequest +import org.store.clothstar.product.dto.response.ProductResponse +import org.store.clothstar.product.service.ProductApplicationService +import org.store.clothstar.product.service.ProductSellerApplicationService + +@Tag(name = "ProductSellers", description = "ProductSellers(판매자) 관련 API 입니다.") +@RestController +@RequestMapping("/v3/sellers/products") +class ProductSellerController( + private val productSellerApplicationService: ProductSellerApplicationService +) { + // 판매자 상품 등록 + @PostMapping + @Operation( + summary = "상품 등록", + description = "카테고리 아이디, 상품 이름, 내용, 가격, 상태를 입력하여 상품을 신규 등록한다." + ) + fun createProduct( + @RequestPart(value = "mainImage", required = false) mainImage: MultipartFile, + @RequestPart(value = "subImages", required = false) subImages: List?, + @RequestPart(value = "dto") @Validated productCreateRequest: ProductCreateRequest + ): ResponseEntity { + // 상품 등록 + productSellerApplicationService.createProduct(mainImage, subImages, productCreateRequest); + + val messageDTO = MessageDTO( + HttpStatus.CREATED.value(), + "상품 생성이 정상적으로 처리됐습니다." + ) + + return ResponseEntity(messageDTO, HttpStatus.CREATED) + } + + // 상품 상세 조회 + @GetMapping("/{productId}") + @Operation(summary = "상품 상세 조회", description = "상품 ID를 사용하여 해당 상품의 상세 정보를 조회합니다.") + fun getProductDetails(@PathVariable productId: Long): ResponseEntity { + val productResponse = productSellerApplicationService.getProductDetails(productId, true) + return ResponseEntity.ok(productResponse) + } + + @PatchMapping("/{productId}/displayStatus") + @Operation(summary = "상품 진열 상태 변경", description = "상품 ID를 사용하여 해당 상품의 진열 상태를 변경합니다.") + fun updateProductDisplayStatus( + @PathVariable productId: Long, + @RequestBody request: UpdateDisplayStatusRequest + ): ResponseEntity { + productSellerApplicationService.updateProductDisplayStatus(productId, request.displayStatus) + + val messageDTO = MessageDTO( + HttpStatus.OK.value(), + "상품 진열 상태가 성공적으로 변경되었습니다." + ) + return ResponseEntity(messageDTO, HttpStatus.OK) + } + + @PatchMapping("/{productId}/items/{itemId}/displayStatus") + @Operation(summary = "아이템 진열 상태 변경", description = "상품 ID와 아이템 ID를 사용하여 해당 아이템의 진열 상태를 변경합니다.") + fun updateItemDisplayStatus( + @PathVariable productId: Long, + @PathVariable itemId: Long, + @RequestBody request: UpdateDisplayStatusRequest + ): ResponseEntity { + productSellerApplicationService.updateItemDisplayStatus(productId, itemId, request.displayStatus) + + val messageDTO = MessageDTO( + HttpStatus.OK.value(), + "아이템 진열 상태가 성공적으로 변경되었습니다." + ) + return ResponseEntity(messageDTO, HttpStatus.OK) + } + + // 상품 재고 수량 변경 + @PatchMapping("/{productId}/items/{itemId}/stock") + @Operation(summary = "아이템 재고 수량 변경", description = "상품 ID와 아이템 ID를 사용하여 해당 아이템의 재고 수량을 변경합니다.") + fun updateItemStock( + @PathVariable productId: Long, + @PathVariable itemId: Long, + @RequestBody request: UpdateStockRequest + ): ResponseEntity { + productSellerApplicationService.updateItemStock(productId, itemId, request.stock) + + val messageDTO = MessageDTO( + HttpStatus.OK.value(), + "아이템 재고 수량이 성공적으로 변경되었습니다." + ) + return ResponseEntity(messageDTO, HttpStatus.OK) + } + + /* + // 상품 기본 정보 수정 + @PutMapping("/{productId}") + @Operation(summary = "상품 기본 정보 수정", description = "상품 ID를 사용하여 해당 상품의 기본 정보를 수정합니다.") + fun updateProduct( + @PathVariable productId: Long, + @RequestPart(value = "mainImage", required = false) mainImage: MultipartFile?, + @RequestPart(value = "subImages", required = false) subImages: List?, + @RequestPart(value = "dto") @Validated productCreateRequest: ProductCreateRequest + ): ResponseEntity { + productApplicationService.updateProduct(productId, mainImage, subImages, productCreateRequest) + + val messageDTO = MessageDTO( + HttpStatus.OK.value(), + "상품 기본 정보가 성공적으로 변경되었습니다." + ) + return ResponseEntity(messageDTO, HttpStatus.OK) + } + + // 상품 옵션&아이템 변경 + @PutMapping("/{productId}/options") + @Operation(summary = "상품 옵션&아이템 변경", description = "상품 ID를 사용하여 해당 상품의 옵션과 아이템을 변경합니다.") + fun updateProductOptions( + @PathVariable productId: Long, + @RequestBody productCreateRequest: ProductCreateRequest + ): ResponseEntity { + productApplicationService.updateProductOptions(productId, productCreateRequest) + + val messageDTO = MessageDTO( + HttpStatus.OK.value(), + "상품 옵션&아이템이 성공적으로 변경되었습니다." + ) + return ResponseEntity(messageDTO, HttpStatus.OK) + } + + */ +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/domain/Product.kt b/src/main/kotlin/org/store/clothstar/product/domain/Product.kt index 066e501..70a2918 100644 --- a/src/main/kotlin/org/store/clothstar/product/domain/Product.kt +++ b/src/main/kotlin/org/store/clothstar/product/domain/Product.kt @@ -1,5 +1,6 @@ package org.store.clothstar.product.domain +import com.fasterxml.jackson.annotation.JsonIgnore import jakarta.persistence.* import org.hibernate.annotations.Fetch import org.hibernate.annotations.FetchMode @@ -46,16 +47,16 @@ class Product( var saleStatus: SaleStatus, // 판매 상태 // 연관 관계 (1:N) + @JsonIgnore @Fetch(FetchMode.SUBSELECT) @OneToMany(mappedBy = "product", cascade = [CascadeType.ALL], fetch = FetchType.LAZY) var productOptions: MutableList = mutableListOf(), + @JsonIgnore @Fetch(FetchMode.SUBSELECT) @OneToMany(mappedBy = "product", fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) var items: MutableList = mutableListOf(), ) : BaseEntity() { - - fun updateSaleStatus(saleStatus: SaleStatus) { this.saleStatus = saleStatus } diff --git a/src/main/kotlin/org/store/clothstar/product/dto/request/UpdateStockRequest.kt b/src/main/kotlin/org/store/clothstar/product/dto/request/UpdateStockRequest.kt new file mode 100644 index 0000000..2375090 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/product/dto/request/UpdateStockRequest.kt @@ -0,0 +1,11 @@ +package org.store.clothstar.product.dto.request + +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.PositiveOrZero + +class UpdateStockRequest ( + + @field:NotNull(message = "재고 수량은 필수 입력 사항입니다.") + @field:PositiveOrZero(message = "재고 수량은 0 이상이어야 합니다.") + val stock: Int +) \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/dto/response/ProductListResponse.kt b/src/main/kotlin/org/store/clothstar/product/dto/response/ProductListResponse.kt new file mode 100644 index 0000000..4131928 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/product/dto/response/ProductListResponse.kt @@ -0,0 +1,44 @@ +package org.store.clothstar.product.dto.response + +import io.swagger.v3.oas.annotations.media.Schema +import org.store.clothstar.member.dto.response.SellerSimpleResponse +import org.store.clothstar.product.domain.Item +import org.store.clothstar.product.domain.Product +import org.store.clothstar.product.domain.type.SaleStatus + +class ProductListResponse( + @Schema(description = "상품 id", example = "1") + val productId: Long, + + @Schema(description = "상품 이름", example = "우유 모자") + val name: String, + + @Schema(description = "상품 설명", example = "우유 모자입니다.") + val content: String, + + @Schema(description = "상품 가격", example = "10000") + val price: Int = 0, + + @Schema(description = "상품 상태", example = "FOR_SALE") + val saleStatus: SaleStatus, + + @Schema(description = "상품 판매량", example = "10") + val saleCount: Long, // ~개 판매중 + + @Schema(description = "판매자 정보") + val seller: SellerSimpleResponse, +) { + companion object { + fun from(product: Product, seller: SellerSimpleResponse): ProductListResponse { + return ProductListResponse( + productId = product.productId!!, + name = product.name, + price = product.price, + saleCount = product.saleCount, + content = product.content, + saleStatus = product.saleStatus, + seller = seller + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/dto/response/ProductResponse.kt b/src/main/kotlin/org/store/clothstar/product/dto/response/ProductResponse.kt index f681adb..88c93bf 100644 --- a/src/main/kotlin/org/store/clothstar/product/dto/response/ProductResponse.kt +++ b/src/main/kotlin/org/store/clothstar/product/dto/response/ProductResponse.kt @@ -1,5 +1,7 @@ package org.store.clothstar.product.dto.response +import org.store.clothstar.member.domain.Seller +import org.store.clothstar.member.dto.response.SellerSimpleResponse import org.store.clothstar.product.domain.* import org.store.clothstar.product.domain.type.DisplayStatus import org.store.clothstar.product.domain.type.ImageType @@ -33,10 +35,22 @@ class ProductResponse( val productColors: Set, val imageList: List, val productOptions: List, - val items: List + val items: List, + val seller: SellerSimpleResponse ) { companion object { - fun from(product: Product): ProductResponse { + fun from(product: Product, + seller: SellerSimpleResponse, + isSeller: Boolean + ): ProductResponse { + + val items = if (isSeller) { + product.items.map { ItemResponse.from(it) } + } else { + product.items.filter { it.displayStatus == DisplayStatus.VISIBLE } + .map { ItemResponse.from(it) } + } + return ProductResponse( id = product.productId!!, name = product.name, @@ -47,7 +61,8 @@ class ProductResponse( displayStatus = product.displayStatus, saleStatus = product.saleStatus, productOptions = product.productOptions.map { ProductOptionResponse.from(it) }, - items = product.items.map { ItemResponse.from(it) } + items = items, + seller = seller ) } } diff --git a/src/main/kotlin/org/store/clothstar/product/repository/ProductRepository.kt b/src/main/kotlin/org/store/clothstar/product/repository/ProductRepository.kt index ebe40e4..6900861 100644 --- a/src/main/kotlin/org/store/clothstar/product/repository/ProductRepository.kt +++ b/src/main/kotlin/org/store/clothstar/product/repository/ProductRepository.kt @@ -3,11 +3,8 @@ package org.store.clothstar.product.repository import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository import org.store.clothstar.product.domain.Product -import java.util.* @Repository -interface ProductRepository : JpaRepository { +interface ProductRepository : JpaRepository, ProductRepositoryCustom { fun findByProductIdIn(productLineIds: List): List - - fun findWithDetailsByProductId(productId: Long): Optional } \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/repository/ProductRepositoryCustom.kt b/src/main/kotlin/org/store/clothstar/product/repository/ProductRepositoryCustom.kt new file mode 100644 index 0000000..7429e17 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/product/repository/ProductRepositoryCustom.kt @@ -0,0 +1,27 @@ +package org.store.clothstar.product.repository + +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice +import org.springframework.stereotype.Repository +import org.store.clothstar.product.domain.Product + +@Repository +interface ProductRepositoryCustom { + + fun findAllOffsetPaging(pageable: Pageable, keyword: String?): Page + + fun findAllSlicePaging(pageable: Pageable, keyword: String?): Slice + + fun findEntitiesByCategoryWithOffsetPaging( + categoryId: Long, + pageable: Pageable, + keyword: String?, + ): Page + + fun findEntitiesByCategoryWithSlicePaging( + categoryId: Long, + pageable: Pageable, + keyword: String?, + ): Slice +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/repository/ProductRepositoryCustomImpl.kt b/src/main/kotlin/org/store/clothstar/product/repository/ProductRepositoryCustomImpl.kt new file mode 100644 index 0000000..e4a17d4 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/product/repository/ProductRepositoryCustomImpl.kt @@ -0,0 +1,158 @@ +package org.store.clothstar.product.repository + +import com.querydsl.core.types.OrderSpecifier +import com.querydsl.core.types.dsl.BooleanExpression +import com.querydsl.jpa.impl.JPAQuery +import com.querydsl.jpa.impl.JPAQueryFactory +import org.springframework.data.domain.* +import org.springframework.data.support.PageableExecutionUtils +import org.springframework.stereotype.Component +import org.store.clothstar.product.domain.Product +import org.store.clothstar.product.domain.QProduct.product +import org.store.clothstar.product.domain.type.DisplayStatus +import java.util.function.LongSupplier + +@Component +class ProductRepositoryCustomImpl( + private val queryFactory: JPAQueryFactory, +) : ProductRepositoryCustom { + + override fun findAllOffsetPaging(pageable: Pageable, keyword: String?): Page { + val content: MutableList = getProductLines(pageable, keyword) + + var hasNext = false + if (content.size > pageable.pageSize) { + content.removeAt(content.size - 1) + hasNext = true + } + + val totalCount: JPAQuery = queryFactory + .select(product.countDistinct()) + .from(product) + .where( + product.deletedAt.isNull() + .and(getSearchCondition(keyword)) + ) + + return PageableExecutionUtils.getPage(content, pageable) { totalCount.fetchOne()!! } + } + + override fun findAllSlicePaging(pageable: Pageable, keyword: String?): Slice { + val content: MutableList = getProductLines(pageable, keyword) + + var hasNext = false + if (content.size > pageable.pageSize) { + content.removeAt(content.size - 1) + hasNext = true + } + + return SliceImpl(content, pageable, hasNext) + } + + private fun getProductLines(pageable: Pageable, keyword: String?): MutableList { + val orderSpecifiers = getOrderSpecifiers(pageable.sort) + + // 카테고리별로 ProductLine 엔티티를 가져옴 + return queryFactory + .selectDistinct(product) + .from(product) + .where( + product.deletedAt.isNull() + .and(product.displayStatus.eq(DisplayStatus.VISIBLE)) + .and(getSearchCondition(keyword)) + ) + .orderBy(*orderSpecifiers.toTypedArray()) // * -> 스프레드 연산자 : 배열의 각 요소를 개별 인자로 전달 + .offset(pageable.offset) + .limit((pageable.pageSize + 1).toLong()) + .fetch() + } + + override fun findEntitiesByCategoryWithOffsetPaging( + categoryId: Long, + pageable: Pageable, + keyword: String? + ): Page { + val content = getProductsByCategory(categoryId, pageable, keyword) + + var hasNext = false + if (content.size > pageable.pageSize) { + content.removeAt(content.size - 1) + hasNext = true + } + + val totalCount = queryFactory + .select(product.countDistinct()) + .from(product) + .where( + product.categoryId.eq(categoryId) + .and(product.deletedAt.isNull()) + .and(product.displayStatus.eq(DisplayStatus.VISIBLE)) + .and(getSearchCondition(keyword)) + ) + + return PageableExecutionUtils.getPage(content, pageable, LongSupplier { totalCount.fetchOne()!! }) + } + + override fun findEntitiesByCategoryWithSlicePaging( + categoryId: Long, + pageable: Pageable, + keyword: String? + ): Slice { + val content = getProductsByCategory(categoryId, pageable, keyword) + + var hasNext = false + if (content.size > pageable.pageSize) { + content.removeAt(content.size - 1) + hasNext = true + } + + return SliceImpl(content, pageable, hasNext) + } + + private fun getProductsByCategory( + categoryId: Long, + pageable: Pageable, + keyword: String? + ): MutableList { + val orderSpecifiers = getOrderSpecifiers(pageable.sort) + + // 카테고리별로 Product 엔티티를 가져옴 + return queryFactory + .selectDistinct(product) + .from(product) + .where( + product.categoryId.eq(categoryId) + .and(product.deletedAt.isNull()) + .and(product.displayStatus.eq(DisplayStatus.VISIBLE)) + .and(getSearchCondition(keyword)) + ) + .orderBy(*orderSpecifiers.toTypedArray()) + .offset(pageable.offset) + .limit((pageable.pageSize + 1).toLong()) + .fetch() + } + + private fun getSearchCondition(keyword: String?): BooleanExpression { + if (keyword.isNullOrEmpty()) { + return product.isNotNull() // 조건이 없을 경우 항상 true를 반환 + } + return product.name.containsIgnoreCase(keyword) + .or(product.content.containsIgnoreCase(keyword)) + } + + private fun getOrderSpecifiers(sort: Sort?): List> { + val orderSpecifiers = mutableListOf>() + + sort?.forEach { order -> + val orderSpecifier = when (order.property) { + "saleCount" -> if (order.isAscending) product.saleCount.asc() else product.saleCount.desc() + "createdAt" -> if (order.isAscending) product.createdAt.asc() else product.createdAt.desc() + "price" -> if (order.isAscending) product.price.asc() else product.price.desc() + else -> null + } + orderSpecifier?.let { orderSpecifiers.add(it) } + } + + return orderSpecifiers + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/repository/ProductRepositoryImpl.kt b/src/main/kotlin/org/store/clothstar/product/repository/ProductRepositoryImpl.kt new file mode 100644 index 0000000..a25605a --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/product/repository/ProductRepositoryImpl.kt @@ -0,0 +1,4 @@ +package org.store.clothstar.product.repository + +interface ProductRepositoryImpl { +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/service/ItemService.kt b/src/main/kotlin/org/store/clothstar/product/service/ItemService.kt index e63daf3..6fccbb6 100644 --- a/src/main/kotlin/org/store/clothstar/product/service/ItemService.kt +++ b/src/main/kotlin/org/store/clothstar/product/service/ItemService.kt @@ -96,6 +96,6 @@ class ItemService( fun getItemByIdAndProductId(itemId: Long, productId: Long): Item { return itemRepository.findByItemIdAndProduct_ProductId(itemId, productId) - ?: throw EntityNotFoundException("Item not found with id: $itemId and productId: $productId") + ?: throw EntityNotFoundException("productId: $productId, itemId: $itemId 인 아이템을 찾을 수 없습니다.") } } \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/service/ProductApplicationService.kt b/src/main/kotlin/org/store/clothstar/product/service/ProductApplicationService.kt index d670f94..f75172a 100644 --- a/src/main/kotlin/org/store/clothstar/product/service/ProductApplicationService.kt +++ b/src/main/kotlin/org/store/clothstar/product/service/ProductApplicationService.kt @@ -1,17 +1,15 @@ package org.store.clothstar.product.service +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import org.springframework.web.multipart.MultipartFile -import org.store.clothstar.member.domain.Member -import org.store.clothstar.member.domain.vo.MemberShoppingActivity import org.store.clothstar.member.service.MemberService -import org.store.clothstar.product.domain.ProductImage -import org.store.clothstar.product.domain.type.DisplayStatus -import org.store.clothstar.product.domain.type.ImageType -import org.store.clothstar.product.dto.request.ProductCreateRequest +import org.store.clothstar.product.dto.response.ProductListResponse import org.store.clothstar.product.dto.response.ProductResponse + @Service class ProductApplicationService( private val productService: ProductService, @@ -19,68 +17,19 @@ class ProductApplicationService( private val itemService: ItemService, private val memberService: MemberService ) { - @Transactional - fun createProduct( - mainImage: MultipartFile, - subImages: List?, - productCreateRequest: ProductCreateRequest - ) { - // get current member (principal) - val memberId: Long = 1 // currentMember.getId() -// val member: Member = memberService.getMemberByMemberId(memberId) - val member = Member(memberId, "010-1234-5678", "Ogu", MemberShoppingActivity.init()) - - // 1. 상품 생성 - val product = productCreateRequest.toProductEntity() - - // 상품 저장 - val savedProduct = productService.createProduct(product) - - // S3에 이미지 업로드 및 URL 생성 - // 2. 메인 이미지 업로드 및 저장 -// val mainImageUrl = s3Service.uploadFile(mainImage) -// val mainImageUrl = s3Service.uploadFile(mainImage) - val mainImageUrl = - "https://on.com2us.com/wp-content/uploads/2023/12/VxdEKDNZCp9hAW5TU5-3MZTePLGSdlYKzEZUyVLDB-Cyo950Ee19yaOL8ayxgJzEfMYfzfLcRYuwkmKEs2cg0w.webp" - product.imageList.add(ProductImage(mainImageUrl, mainImageUrl, ImageType.MAIN)) - - // 3. 서브 이미지 업로드 및® 저장 - subImages?.forEach { subImage -> -// val subImageUrl = s3Service.uploadFile(subImage) - val subImageUrl = - "https://on.com2us.com/wp-content/uploads/2023/12/%EC%98%A4%EA%B5%AC%EC%99%80-%EB%B9%84%EB%B0%80%EC%9D%98%EC%88%B2-002.jpg" - product.imageList.add(ProductImage(subImageUrl, subImageUrl, ImageType.SUB)) - } - - - // 4. 상품 옵션 생성 - val productOptions = productCreateRequest.productOptions.map { optionRequest -> - val productOption = productOptionService.createProductOption(product, optionRequest) - product.productOptions.add(productOption) - } - - // 5. 상품 아이템 생성 - productCreateRequest.items.forEach { itemRequest -> - val item = itemService.createItem(product, itemRequest) - product.items.add(item) - } - } @Transactional(readOnly = true) - fun getProductDetails(productId: Long): ProductResponse { - - return productService.getProductDetails(productId) + fun getProductDetails(productId: Long, isSeller: Boolean): ProductResponse { + return productService.getProductDetails(productId, isSeller) } - @Transactional - fun updateProductDisplayStatus(productId: Long, displayStatus: DisplayStatus) { - val product = productService.getProductById(productId) - product.updateDisplayStatus(displayStatus) + @Transactional(readOnly = true) + fun getAllProductsOffsetPaging(pageable: Pageable, keyword: String?): Page { + return productService.getAllProductsOffsetPaging(pageable, keyword) } - @Transactional - fun updateItemDisplayStatus(productId: Long, itemId: Long, displayStatus: DisplayStatus) { - val item = itemService.getItemByIdAndProductId(itemId, productId) - item.updateDisplayStatus(displayStatus) + @Transactional(readOnly = true) + fun getAllProductsSlicePaging(pageable: Pageable, keyword: String?): Slice { + return productService.getAllProductsSlicePaging(pageable, keyword) } } \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/service/ProductSellerApplicationService.kt b/src/main/kotlin/org/store/clothstar/product/service/ProductSellerApplicationService.kt new file mode 100644 index 0000000..410f4bf --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/product/service/ProductSellerApplicationService.kt @@ -0,0 +1,94 @@ +package org.store.clothstar.product.service + +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import org.store.clothstar.member.authentication.domain.CustomUserDetails +import org.store.clothstar.member.service.MemberService +import org.store.clothstar.product.domain.ProductImage +import org.store.clothstar.product.domain.type.DisplayStatus +import org.store.clothstar.product.domain.type.ImageType +import org.store.clothstar.product.dto.request.ProductCreateRequest +import org.store.clothstar.product.dto.response.ProductResponse + +@Service +class ProductSellerApplicationService ( + private val productService: ProductService, + private val productOptionService: ProductOptionService, + private val itemService: ItemService, + private val memberService: MemberService +) { + @Transactional + fun createProduct( + mainImage: MultipartFile, + subImages: List?, + productCreateRequest: ProductCreateRequest + ) { + val member = SecurityContextHolder.getContext().authentication.principal as CustomUserDetails + +// if(memberService.isSeller(member.id)) { +// throw IllegalArgumentException("판매자만 상품을 등록할 수 있습니다.") +// } + + // 1. 상품 생성 + val product = productCreateRequest.toProductEntity() + + // 상품 저장 + val savedProduct = productService.createProduct(product) + + // S3에 이미지 업로드 및 URL 생성 + // 2. 메인 이미지 업로드 및 저장 +// val mainImageUrl = s3Service.uploadFile(mainImage) +// val mainImageUrl = s3Service.uploadFile(mainImage) + val mainImageUrl = + "https://on.com2us.com/wp-content/uploads/2023/12/VxdEKDNZCp9hAW5TU5-3MZTePLGSdlYKzEZUyVLDB-Cyo950Ee19yaOL8ayxgJzEfMYfzfLcRYuwkmKEs2cg0w.webp" + product.imageList.add(ProductImage(mainImageUrl, mainImageUrl, ImageType.MAIN)) + + // 3. 서브 이미지 업로드 및® 저장 + subImages?.forEach { subImage -> +// val subImageUrl = s3Service.uploadFile(subImage) + val subImageUrl = + "https://on.com2us.com/wp-content/uploads/2023/12/%EC%98%A4%EA%B5%AC%EC%99%80-%EB%B9%84%EB%B0%80%EC%9D%98%EC%88%B2-002.jpg" + product.imageList.add(ProductImage(subImageUrl, subImageUrl, ImageType.SUB)) + } + + + // 4. 상품 옵션 생성 + val productOptions = productCreateRequest.productOptions.map { optionRequest -> + val productOption = productOptionService.createProductOption(product, optionRequest) + product.productOptions.add(productOption) + } + + // 5. 상품 아이템 생성 + productCreateRequest.items.forEach { itemRequest -> + val item = itemService.createItem(product, itemRequest) + product.items.add(item) + } + } + + @Transactional(readOnly = true) + fun getProductDetails(productId: Long, isSeller: Boolean): ProductResponse { + return productService.getProductDetails(productId, isSeller) + } + + @Transactional + fun updateProductDisplayStatus(productId: Long, displayStatus: DisplayStatus) { + val product = productService.getProductById(productId) + product.updateDisplayStatus(displayStatus) + } + + @Transactional + fun updateItemDisplayStatus(productId: Long, itemId: Long, displayStatus: DisplayStatus) { + val item = itemService.getItemByIdAndProductId(itemId, productId) + item.updateDisplayStatus(displayStatus) + } + + @Transactional + fun updateItemStock(productId: Long, itemId: Long, stock: Int) { +// val Member = getPrin + + val item = itemService.getItemByIdAndProductId(itemId, productId) + item.updateStock(stock) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/service/ProductService.kt b/src/main/kotlin/org/store/clothstar/product/service/ProductService.kt index 6cc9f83..addd797 100644 --- a/src/main/kotlin/org/store/clothstar/product/service/ProductService.kt +++ b/src/main/kotlin/org/store/clothstar/product/service/ProductService.kt @@ -1,18 +1,25 @@ package org.store.clothstar.product.service import jakarta.persistence.EntityNotFoundException +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import org.springframework.web.server.ResponseStatusException +import org.store.clothstar.member.dto.response.SellerSimpleResponse +import org.store.clothstar.member.service.SellerService import org.store.clothstar.product.domain.Product +import org.store.clothstar.product.dto.response.ProductListResponse import org.store.clothstar.product.dto.response.ProductResponse import org.store.clothstar.product.repository.ProductRepository @Service class ProductService( private val productRepository: ProductRepository, + private val sellerService: SellerService, ) { @Transactional fun createProduct(product: Product): Product { @@ -20,11 +27,69 @@ class ProductService( } @Transactional(readOnly = true) - fun getProductDetails(productId: Long): ProductResponse { - val product = productRepository.findWithDetailsByProductId(productId) - .orElseThrow { EntityNotFoundException("상품을 찾을 수 없습니다.") } + fun getProductDetails(productId: Long, isSeller: Boolean): ProductResponse { + val product = productRepository.findByIdOrNull(productId) + ?: throw EntityNotFoundException("상품을 찾을 수 없습니다.") - return ProductResponse.from(product) + val seller = sellerService.getSellerById(product.memberId) + val sellerSimpleResponse = SellerSimpleResponse.getSellerSimpleResponseBySeller(seller) + return ProductResponse.from(product, sellerSimpleResponse, isSeller) + } + + @Transactional + fun getProductLinesByCategoryWithOffsetPaging( + categoryId: Long, + pageable: Pageable, + keyword: String?, + ): Page { + val allOffsetPagingByCategory = + productRepository.findEntitiesByCategoryWithOffsetPaging(categoryId, pageable, keyword) + + return allOffsetPagingByCategory.map { product -> + val seller = sellerService.getSellerById(product.memberId) + val sellerSimpleResponse = SellerSimpleResponse.getSellerSimpleResponseBySeller(seller) + ProductListResponse.from(product, sellerSimpleResponse) + } + } + + @Transactional + fun getProductLinesByCategoryWithSlicePaging( + categoryId: Long, + pageable: Pageable, + keyword: String?, + ): Slice { + val allSlicePagingByCategory = + productRepository.findEntitiesByCategoryWithSlicePaging(categoryId, pageable, keyword) + + return allSlicePagingByCategory.map { product -> + val seller = sellerService.getSellerById(product.memberId) + val sellerSimpleResponse = SellerSimpleResponse.getSellerSimpleResponseBySeller(seller) + ProductListResponse.from(product, sellerSimpleResponse) + } + } + + fun getAllProductsOffsetPaging(pageable: Pageable, keyword: String? + ): Page { + + val productPages = productRepository.findAllOffsetPaging(pageable, keyword) + + return productPages.map { product -> + val seller = sellerService.getSellerById(product.memberId) + val sellerSimpleResponse = SellerSimpleResponse.getSellerSimpleResponseBySeller(seller) + ProductListResponse.from(product, sellerSimpleResponse) + } + } + + fun getAllProductsSlicePaging(pageable: Pageable, keyword: String? + ): Slice { + + val productPages = productRepository.findAllSlicePaging(pageable, keyword) + + return productPages.map { product -> + val seller = sellerService.getSellerById(product.memberId) + val sellerSimpleResponse = SellerSimpleResponse.getSellerSimpleResponseBySeller(seller) + ProductListResponse.from(product, sellerSimpleResponse) + } } fun findByProductIdIn(productIds: List): List { diff --git a/src/main/resources/application-db.yml b/src/main/resources/application-db.yml index 4e2c36a..708ba4f 100644 --- a/src/main/resources/application-db.yml +++ b/src/main/resources/application-db.yml @@ -30,7 +30,7 @@ spring: jpa: properties: hibernate: - default_batch_fetch_size: 200 + default_batch_fetch_size: 100 --- # local 공통 설정 spring: diff --git a/src/test/kotlin/org/store/clothstar/product/service/ProductApplicationServiceTest.kt b/src/test/kotlin/org/store/clothstar/product/service/ProductApplicationServiceTest.kt index e2d01fa..b807b5e 100644 --- a/src/test/kotlin/org/store/clothstar/product/service/ProductApplicationServiceTest.kt +++ b/src/test/kotlin/org/store/clothstar/product/service/ProductApplicationServiceTest.kt @@ -33,6 +33,9 @@ class ProductApplicationServiceTest { @InjectMocks private lateinit var productApplicationService: ProductApplicationService + @InjectMocks + private lateinit var productSellerApplicationService: ProductSellerApplicationService + @Disabled @DisplayName("유효한 ProductCreateRequest가 들어오면 상품 생성에 성공한다.") @Test @@ -73,7 +76,7 @@ class ProductApplicationServiceTest { given(productService.getProductById(productId)).willReturn(mockProduct) // when - productApplicationService.updateProductDisplayStatus(productId, displayStatus) + productSellerApplicationService.updateProductDisplayStatus(productId, displayStatus) // then verify(productService).getProductById(productId) @@ -92,7 +95,7 @@ class ProductApplicationServiceTest { given(itemService.getItemByIdAndProductId(itemId, productId)).willReturn(mockItem) // when - productApplicationService.updateItemDisplayStatus(productId, itemId, displayStatus) + productSellerApplicationService.updateItemDisplayStatus(productId, itemId, displayStatus) // then verify(itemService).getItemByIdAndProductId(itemId, productId)