Skip to content

Commit

Permalink
refactor: 상품 상세 조회 api 응답 필드 수정 (#92)
Browse files Browse the repository at this point in the history
* feature: ProductDescription 도메인 객체 추가

* chore: Rebase conflict 해결

* refactor: ProductDescription 없는 경우 반영하도록 수정

* refactor: ImageType 변수명 변경

* chore: 더미 데이터에 변경 내용 반영

* refactor: Description, Option, Info 의 외래키를 Product 가 관리하도록 변경

* refactor: Product 필드 기본값 수정
  • Loading branch information
Combi153 authored Feb 20, 2024
1 parent 9278ad9 commit 011dd7e
Show file tree
Hide file tree
Showing 25 changed files with 430 additions and 172 deletions.
20 changes: 17 additions & 3 deletions src/main/kotlin/com/petqua/application/product/ProductService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import com.petqua.domain.auth.LoginMemberOrGuest
import com.petqua.domain.keyword.ProductKeywordRepository
import com.petqua.domain.product.ProductRepository
import com.petqua.domain.product.WishProductRepository
import com.petqua.domain.product.detail.ProductImageRepository
import com.petqua.domain.product.detail.image.ImageType
import com.petqua.domain.product.detail.image.ImageType.DESCRIPTION
import com.petqua.domain.product.detail.image.ImageType.SAMPLE
import com.petqua.domain.product.detail.image.ProductImageRepository
import com.petqua.domain.product.dto.ProductResponse
import com.petqua.exception.product.ProductException
import com.petqua.exception.product.ProductExceptionType.NOT_FOUND_PRODUCT
Expand All @@ -31,13 +34,24 @@ class ProductService(
val productWithInfo = productRepository.findProductWithInfoByIdOrThrow(productId) {
ProductException(NOT_FOUND_PRODUCT)
}
val imageUrls = productImageRepository.findProductImagesByProductId(productId).map { it.imageUrl }

val imagesByType = getProductImagesGroupedByType(productId)
val isWished = loginMemberOrGuest.isMember() && wishProductRepository.existsByProductIdAndMemberId(
productId = productId,
memberId = loginMemberOrGuest.memberId
)

return ProductDetailResponse(productWithInfo, imageUrls, isWished)
return ProductDetailResponse(
productWithInfoResponse = productWithInfo,
imageUrls = imagesByType[SAMPLE] ?: emptyList(),
descriptionImageUrls = imagesByType[DESCRIPTION] ?: emptyList(),
isWished = isWished
)
}

private fun getProductImagesGroupedByType(productId: Long): Map<ImageType, List<String>> {
return productImageRepository.findProductImagesByProductId(productId).groupBy { it.imageType }
.mapValues { it.value.map { productImage -> productImage.imageUrl } }
}

@Transactional(readOnly = true)
Expand Down
30 changes: 19 additions & 11 deletions src/main/kotlin/com/petqua/application/product/dto/ProductDtos.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,23 +83,29 @@ data class ProductDetailResponse(
)
val reviewAverageScore: Double,

@Schema(
description = "상품 썸네일 이미지",
example = "https://docs.petqua.co.kr/products/thumbnails/thumbnail1.jpeg"
)
val thumbnailUrl: String,

@Schema(
description = "상품 이미지 목록",
example = "[image1.jpeg, image2.jpeg]"
)
val imageUrls: List<String>,

@Schema(
description = "상품 상세 설명",
example = "귀엽습니다"
description = "상품 상세 설명 제목",
example = "물생활 핵 인싸어, 레드 브론즈 구피"
)
val descriptionTitle: String,

@Schema(
description = "상품 상세 설명 내용",
example = "레드 턱시도라고도 불리며 지느러미가 아름다운 구피입니다"
)
val descriptionContent: String,

@Schema(
description = "상품 상세 이미지",
example = "[image1.jpeg, image2.jpeg]"
)
val description: String,
val descriptionImageUrls: List<String>,

@Schema(
description = "안전 배송 가능 여부",
Expand Down Expand Up @@ -164,6 +170,7 @@ data class ProductDetailResponse(
constructor(
productWithInfoResponse: ProductWithInfoResponse,
imageUrls: List<String>,
descriptionImageUrls: List<String>,
isWished: Boolean,
) : this(
id = productWithInfoResponse.id,
Expand All @@ -177,9 +184,10 @@ data class ProductDetailResponse(
wishCount = productWithInfoResponse.wishCount,
reviewCount = productWithInfoResponse.reviewCount,
reviewAverageScore = productWithInfoResponse.reviewAverageScore,
thumbnailUrl = productWithInfoResponse.thumbnailUrl,
description = productWithInfoResponse.description,
imageUrls = imageUrls,
descriptionTitle = productWithInfoResponse.descriptionTitle,
descriptionContent = productWithInfoResponse.descriptionContent,
descriptionImageUrls = descriptionImageUrls,
canDeliverSafely = productWithInfoResponse.canDeliverSafely,
canDeliverCommonly = productWithInfoResponse.canDeliverCommonly,
canPickUp = productWithInfoResponse.canPickUp,
Expand Down
104 changes: 66 additions & 38 deletions src/main/kotlin/com/petqua/common/config/DataInitializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@ import com.petqua.domain.product.category.Category
import com.petqua.domain.product.category.CategoryRepository
import com.petqua.domain.product.category.Family
import com.petqua.domain.product.category.Species
import com.petqua.domain.product.detail.DifficultyLevel.NORMAL
import com.petqua.domain.product.detail.OptimalTankSize.TANK2
import com.petqua.domain.product.detail.OptimalTemperature
import com.petqua.domain.product.detail.ProductImage
import com.petqua.domain.product.detail.ProductImageRepository
import com.petqua.domain.product.detail.ProductInfo
import com.petqua.domain.product.detail.ProductInfoRepository
import com.petqua.domain.product.detail.Temperament.PEACEFUL
import com.petqua.domain.product.detail.description.ProductDescription
import com.petqua.domain.product.detail.description.ProductDescriptionContent
import com.petqua.domain.product.detail.description.ProductDescriptionRepository
import com.petqua.domain.product.detail.description.ProductDescriptionTitle
import com.petqua.domain.product.detail.image.ImageType.DESCRIPTION
import com.petqua.domain.product.detail.image.ImageType.SAMPLE
import com.petqua.domain.product.detail.image.ProductImage
import com.petqua.domain.product.detail.image.ProductImageRepository
import com.petqua.domain.product.detail.info.DifficultyLevel.NORMAL
import com.petqua.domain.product.detail.info.OptimalTankSize.TANK2
import com.petqua.domain.product.detail.info.OptimalTemperature
import com.petqua.domain.product.detail.info.ProductInfo
import com.petqua.domain.product.detail.info.ProductInfoRepository
import com.petqua.domain.product.detail.info.Temperament.PEACEFUL
import com.petqua.domain.product.option.ProductOption
import com.petqua.domain.product.option.ProductOptionRepository
import com.petqua.domain.product.option.Sex.FEMALE
Expand All @@ -46,6 +52,7 @@ import org.springframework.context.annotation.Profile
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import kotlin.random.Random

@Component
@Profile("local", "prod")
Expand All @@ -64,6 +71,7 @@ class DataInitializer(
private val productOptionRepository: ProductOptionRepository,
private val wishProductRepository: WishProductRepository,
private val productKeywordRepository: ProductKeywordRepository,
private val productDescriptionRepository: ProductDescriptionRepository,
) {

@EventListener(ApplicationReadyEvent::class)
Expand Down Expand Up @@ -164,6 +172,39 @@ class DataInitializer(
}
val reviewCount = (1..5).random()

val sex = when {
(it % 3) == 0 -> MALE
(it % 7) == 0 -> FEMALE
else -> HERMAPHRODITE
}
val productOption = productOptionRepository.save(
ProductOption(
sex = sex,
additionalPrice = BigDecimal.ZERO
)
)

val productInfo = productInfoRepository.save(
ProductInfo(
categoryId = categoryId,
optimalTemperature = OptimalTemperature(22, 25),
difficultyLevel = NORMAL,
optimalTankSize = TANK2,
temperament = PEACEFUL
)
)

val productDescriptionId = when {
(it % 4) != 0 -> productDescriptionRepository.save(
ProductDescription(
title = ProductDescriptionTitle("물생활 핵 인싸어, 상품$it"),
content = ProductDescriptionContent("지느러미가 아름다운 상품$it 입니다")
)
).id

else -> null
}

Product(
name = "상품$it",
categoryId = categoryId,
Expand All @@ -175,10 +216,12 @@ class DataInitializer(
reviewCount = reviewCount,
reviewTotalScore = (1..reviewCount).sum(),
thumbnailUrl = "https://docs.petqua.co.kr/products/thumbnails/thumbnail3.jpeg",
description = "https://www.goldmoonaqua.com/web/upload/NNEditor/20221226/copy-1672038777-guppy_EC958CEBB984EB85B8ED9280EBA088EB939C_02.png",
canDeliverSafely = canDeliverSafely,
canDeliverCommonly = canDeliverCommonly,
canPickUp = canPickUp,
productOptionId = productOption.id,
productDescriptionId = productDescriptionId,
productInfoId = productInfo.id,
)
}
productRepository.saveAll(products)
Expand Down Expand Up @@ -213,42 +256,27 @@ class DataInitializer(
}
recommendationRepository.saveAll(productRecommendations)

// productOption
val productOptions = products.map {
val sex = when {
(it.id % 3).toInt() == 0 -> MALE
(it.id % 7).toInt() == 0 -> HERMAPHRODITE
else -> FEMALE
// productImage
val productImages = products.flatMap { product ->
List(Random.nextInt(1, 6)) {
ProductImage(
productId = product.id,
imageUrl = "https://docs.petqua.co.kr/products/thumbnails/thumbnail3.jpeg",
imageType = SAMPLE
)
}
ProductOption(
productId = it.id,
sex = sex,
additionalPrice = BigDecimal.ZERO
)
}
productOptionRepository.saveAll(productOptions)

// productInfo
val productInfos = products.map {
ProductInfo(
productId = it.id,
categoryId = it.categoryId,
optimalTemperature = OptimalTemperature(22, 25),
difficultyLevel = NORMAL,
optimalTankSize = TANK2,
temperament = PEACEFUL
)
}
productInfoRepository.saveAll(productInfos)
productImageRepository.saveAll(productImages)

// productImage
val productImages = products.map {
// productDescriptionImage
val productDescriptionImages = products.map {
ProductImage(
productId = it.id,
imageUrl = "https://docs.petqua.co.kr/products/thumbnails/thumbnail3.jpeg"
imageUrl = "https://www.goldmoonaqua.com/web/upload/NNEditor/20221226/copy-1672038777-guppy_EC958CEBB984EB85B8ED9280EBA088EB939C_02.png",
imageType = DESCRIPTION
)
}
productImageRepository.saveAll(productImages)
productImageRepository.saveAll(productDescriptionImages)

// review
val productReviews = products.flatMap { product ->
Expand Down
20 changes: 11 additions & 9 deletions src/main/kotlin/com/petqua/domain/product/Product.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import java.math.BigDecimal

private const val SCALE = 1
private const val ZERO = 0

@Entity
class Product(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Expand All @@ -26,13 +23,13 @@ class Product(
val name: String,

@Column(nullable = false)
val categoryId: Long = 0,
val categoryId: Long,

@Column(nullable = false)
val price: BigDecimal,

@Column(nullable = false)
val storeId: Long = 0,
val storeId: Long,

@Column(nullable = false)
val discountRate: Int = 0,
Expand All @@ -53,9 +50,6 @@ class Product(
@Column(nullable = false)
val thumbnailUrl: String,

@Column(nullable = false)
val description: String,

@Column(nullable = false)
var isDeleted: Boolean = false,

Expand All @@ -67,6 +61,14 @@ class Product(

@Column(nullable = false)
val canPickUp: Boolean,

@Column(nullable = false)
val productOptionId: Long,

val productDescriptionId: Long?,

@Column(nullable = false)
val productInfoId: Long,
) : BaseEntity(), SoftDeleteEntity {

fun averageReviewScore(): Double {
Expand All @@ -88,6 +90,6 @@ class Product(
}

override fun toString(): String {
return "Product(id=$id, name='$name', categoryId=$categoryId, price=$price, storeId=$storeId, discountRate=$discountRate, discountPrice=$discountPrice, wishCount=$wishCount, reviewCount=$reviewCount, reviewTotalScore=$reviewTotalScore, thumbnailUrl='$thumbnailUrl', description='$description', isDeleted=$isDeleted, canDeliverSafely=$canDeliverSafely, canDeliverCommonly=$canDeliverCommonly, canPickUp=$canPickUp)"
return "Product(id=$id, name='$name', categoryId=$categoryId, price=$price, storeId=$storeId, discountRate=$discountRate, discountPrice=$discountPrice, wishCount=$wishCount, reviewCount=$reviewCount, reviewTotalScore=$reviewTotalScore, thumbnailUrl='$thumbnailUrl', isDeleted=$isDeleted, canDeliverSafely=$canDeliverSafely, canDeliverCommonly=$canDeliverCommonly, canPickUp=$canPickUp)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.petqua.domain.product
import com.petqua.common.domain.dto.CursorBasedPaging
import com.petqua.domain.product.dto.ProductReadCondition
import com.petqua.domain.product.dto.ProductResponse
import com.petqua.domain.product.dto.ProductWithInfoResponse
import com.petqua.domain.product.dto.ProductSearchCondition
import com.petqua.domain.product.dto.ProductWithInfoResponse

interface ProductCustomRepository {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import com.petqua.common.util.createSingleQueryOrThrow
import com.petqua.domain.keyword.ProductKeyword
import com.petqua.domain.product.Sorter.ENROLLMENT_DATE_DESC
import com.petqua.domain.product.category.Category
import com.petqua.domain.product.detail.ProductInfo
import com.petqua.domain.product.detail.description.ProductDescription
import com.petqua.domain.product.detail.description.ProductDescriptionContent
import com.petqua.domain.product.detail.description.ProductDescriptionTitle
import com.petqua.domain.product.detail.info.ProductInfo
import com.petqua.domain.product.dto.ProductDescriptionResponse
import com.petqua.domain.product.dto.ProductReadCondition
import com.petqua.domain.product.dto.ProductResponse
import com.petqua.domain.product.dto.ProductSearchCondition
Expand All @@ -21,6 +25,7 @@ import jakarta.persistence.EntityManager
import org.springframework.stereotype.Repository

private const val ESCAPE_LETTER = '\\'
private const val EMPTY_VALUE = ""

@Repository
class ProductCustomRepositoryImpl(
Expand All @@ -37,15 +42,21 @@ class ProductCustomRepositoryImpl(
selectNew<ProductWithInfoResponse>(
entity(Product::class),
path(Store::name),
new(
ProductDescriptionResponse::class,
coalesce(path(ProductDescription::title)(ProductDescriptionTitle::value), EMPTY_VALUE),
coalesce(path(ProductDescription::content)(ProductDescriptionContent::value), EMPTY_VALUE)
),
entity(ProductInfo::class),
entity(Category::class),
entity(ProductOption::class),
).from(
entity(Product::class),
join(Store::class).on(path(Product::storeId).eq(path(Store::id))),
join(ProductInfo::class).on(path(Product::id).eq(path(ProductInfo::productId))),
leftJoin(ProductDescription::class).on(path(Product::productDescriptionId).eq(path(ProductDescription::id))),
join(ProductInfo::class).on(path(Product::productInfoId).eq(path(ProductInfo::id))),
join(Category::class).on(path(Product::categoryId).eq(path(Category::id))),
join(ProductOption::class).on(path(Product::id).eq(path(ProductOption::productId)))
join(ProductOption::class).on(path(Product::productOptionId).eq(path(ProductOption::id)))
).whereAnd(
path(Product::id).eq(id),
active(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.petqua.domain.product.detail.description

import com.petqua.common.domain.BaseEntity
import jakarta.persistence.Embedded
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType.IDENTITY
import jakarta.persistence.Id

@Entity
class ProductDescription(
@Id @GeneratedValue(strategy = IDENTITY)
val id: Long = 0,

@Embedded
val title: ProductDescriptionTitle,

@Embedded
val content: ProductDescriptionContent,
) : BaseEntity() {
}
Loading

0 comments on commit 011dd7e

Please sign in to comment.