Skip to content

Commit

Permalink
Merge pull request #46 from YAPP-Github/feature/geng/issue-44-comment
Browse files Browse the repository at this point in the history
feat : 상품 코멘트 평가 api 구현
  • Loading branch information
kingjakeu authored Jul 8, 2023
2 parents 9548da5 + ed039c8 commit dfa3354
Show file tree
Hide file tree
Showing 19 changed files with 437 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.yapp.cvs.api.comment

import com.yapp.cvs.api.comment.dto.ProductCommentContentDTO
import com.yapp.cvs.api.comment.dto.ProductCommentDTO
import com.yapp.cvs.api.comment.dto.ProductCommentDetailDTO
import com.yapp.cvs.api.comment.dto.ProductCommentSearchDTO
import com.yapp.cvs.api.common.dto.OffsetPageDTO
import com.yapp.cvs.domain.comment.application.ProductCommentProcessor
import com.yapp.cvs.domain.like.application.ProductLikeProcessor
import io.swagger.v3.oas.annotations.Operation
import org.springdoc.api.annotations.ParameterObject
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/v1/product")
class ProductCommentController(
private val productCommentProcessor: ProductCommentProcessor,
private val productLikeProcessor: ProductLikeProcessor
) {
private val memberId = 1L //TODO : security context 에서 memberId 가져오기

@GetMapping("/{productId}/comments")
@Operation(description = "해당 상품에 작성된 평가 코멘트를 조건만큼 가져옵니다.")
fun getProductComments(@PathVariable productId: Long,
@ParameterObject productCommentSearchDTO: ProductCommentSearchDTO): OffsetPageDTO<ProductCommentDetailDTO> {
val result = productCommentProcessor.getCommentDetails(productId, memberId, productCommentSearchDTO.toVO())
return OffsetPageDTO(result.lastId, result.content.map { ProductCommentDetailDTO.from(it) })
}

@PostMapping("/{productId}/comment/write")
@Operation(description = "상품에 대한 평가 코멘트를 작성합니다.")
fun writeComment(@PathVariable productId: Long,
@RequestBody productCommentContentDTO: ProductCommentContentDTO): ProductCommentDTO {
val comment = productCommentProcessor.createComment(productId, memberId, productCommentContentDTO.content)
return ProductCommentDTO.from(comment)
}

@PostMapping("/{productId}/comment/edit")
@Operation(description = "상품에 대한 평가 코멘트를 수정합니다.")
fun updateComment(@PathVariable productId: Long,
@RequestBody productCommentContentDTO: ProductCommentContentDTO): ProductCommentDTO {
val comment = productCommentProcessor.updateComment(productId, memberId, productCommentContentDTO.content)
return ProductCommentDTO.from(comment)
}

@PostMapping("/{productId}/comment/delete")
@Operation(description = "상품에 대한 평가 코멘트를 삭제합니다.")
fun deleteComment(@PathVariable productId: Long) {
productCommentProcessor.inactivateComment(productId, memberId)
productLikeProcessor.cancelEvaluation(productId, memberId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.yapp.cvs.api.comment.dto

data class ProductCommentContentDTO(
val content: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.yapp.cvs.api.comment.dto

import com.yapp.cvs.domain.comment.vo.ProductCommentVO

data class ProductCommentDTO(
val productCommentId: Long,
val productId: Long,
val memberId: Long,
val content: String
) {
companion object {
fun from(productCommentVO: ProductCommentVO): ProductCommentDTO {
return ProductCommentDTO(
productCommentId = productCommentVO.productCommentId,
productId = productCommentVO.productId,
memberId = productCommentVO.memberId,
content = productCommentVO.content
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.yapp.cvs.api.comment.dto

import com.yapp.cvs.domain.comment.vo.ProductCommentDetailVO
import com.yapp.cvs.domain.enums.ProductLikeType
import java.time.LocalDateTime

data class ProductCommentDetailDTO(
val productCommentId: Long,
val content: String,
val commentLikeCount: Long,
val isOwner: Boolean,
val createdAt: LocalDateTime,
val likeType: ProductLikeType,

val memberId: Long,
val nickname: String,

val productId: Long
) {
companion object {
fun from(productCommentDetailVO: ProductCommentDetailVO): ProductCommentDetailDTO {
return ProductCommentDetailDTO(
productCommentId = productCommentDetailVO.productCommentId,
content = productCommentDetailVO.content,
commentLikeCount = productCommentDetailVO.commentLikeCount,
isOwner = productCommentDetailVO.isOwner,
createdAt = productCommentDetailVO.createdAt,
likeType = productCommentDetailVO.likeType ?: ProductLikeType.NONE,
memberId = productCommentDetailVO.memberId,
nickname = productCommentDetailVO.nickname,
productId = productCommentDetailVO.productId
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.yapp.cvs.api.comment.dto

import com.yapp.cvs.domain.comment.entity.ProductCommentOrderType
import com.yapp.cvs.domain.comment.vo.ProductCommentSearchVO


data class ProductCommentSearchDTO(
val pageSize: Long = 10,
val offsetProductCommentId: Long? = null,
val orderBy: ProductCommentOrderType = ProductCommentOrderType.RECENT
) {
fun toVO(): ProductCommentSearchVO {
return ProductCommentSearchVO(
pageSize = pageSize,
offsetProductCommentId = offsetProductCommentId,
orderBy = orderBy
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.yapp.cvs.api.product.dto.ProductDetailDTO
import com.yapp.cvs.api.product.dto.ProductSearchDTO
import com.yapp.cvs.domain.product.application.ProductProcessor
import io.swagger.v3.oas.annotations.Parameter
import org.springdoc.api.annotations.ParameterObject
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
Expand All @@ -22,7 +23,7 @@ class ProductController(
}

@GetMapping("/search")
fun searchProductList(productSearchDTO: ProductSearchDTO): OffsetPageDTO<ProductDTO> {
fun searchProductList(@ParameterObject productSearchDTO: ProductSearchDTO): OffsetPageDTO<ProductDTO> {
val result = productProcessor.searchProductPageList(productSearchDTO.toVO())
return OffsetPageDTO(result.lastId, result.content.map { ProductDTO.from(it) })
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.yapp.cvs.domain.comment.application

import com.yapp.cvs.domain.base.vo.OffsetPageVO
import com.yapp.cvs.domain.comment.vo.ProductCommentDetailVO
import com.yapp.cvs.domain.comment.vo.ProductCommentSearchVO
import com.yapp.cvs.domain.comment.vo.ProductCommentVO
import com.yapp.cvs.domain.enums.DistributedLockType
import com.yapp.cvs.domain.enums.ProductLikeType
import com.yapp.cvs.domain.like.application.ProductLikeHistoryService
import com.yapp.cvs.domain.like.application.ProductLikeSummaryService
import com.yapp.cvs.infrastructure.redis.lock.DistributedLock
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
@Transactional
class ProductCommentProcessor(
private val productCommentService: ProductCommentService
) {
fun getCommentDetails(productId: Long, memberId: Long, productCommentSearchVO: ProductCommentSearchVO): OffsetPageVO<ProductCommentDetailVO> {
val result = productCommentService.findProductCommentsPage(productId, productCommentSearchVO)
result.filter { it.memberId == memberId }
.forEach { it.isOwner = true }
return OffsetPageVO(result.lastOrNull()?.productCommentId, result)
}

@DistributedLock(DistributedLockType.MEMBER_PRODUCT, ["productId", "memberId"])
fun createComment(productId: Long, memberId: Long, content: String): ProductCommentVO {
val commentHistory = productCommentService.write(productId, memberId, content)
return ProductCommentVO.from(commentHistory)
}

@DistributedLock(DistributedLockType.MEMBER_PRODUCT, ["productId", "memberId"])
fun updateComment(productId: Long, memberId: Long, content: String): ProductCommentVO {
val commentHistory = productCommentService.update(productId, memberId, content)
return ProductCommentVO.from(commentHistory)
}

@DistributedLock(DistributedLockType.MEMBER_PRODUCT, ["productId", "memberId"])
fun inactivateComment(productId: Long, memberId: Long) {
productCommentService.inactivate(productId, memberId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.yapp.cvs.domain.comment.application

import com.yapp.cvs.domain.comment.entity.ProductComment
import com.yapp.cvs.domain.comment.repository.ProductCommentRepository
import com.yapp.cvs.domain.comment.vo.ProductCommentDetailVO
import com.yapp.cvs.domain.comment.vo.ProductCommentSearchVO
import com.yapp.cvs.exception.BadRequestException
import com.yapp.cvs.exception.NotFoundSourceException
import org.springframework.stereotype.Service

@Service
class ProductCommentService(
val productCommentRepository: ProductCommentRepository
) {
fun findProductCommentsPage(productId: Long, productCommentSearchVO: ProductCommentSearchVO): List<ProductCommentDetailVO> {
return productCommentRepository.findAllByProductIdAndPageOffset(productId, productCommentSearchVO)
}

fun write(productId: Long, memberId: Long, content: String): ProductComment {
validateCommentDuplication(productId, memberId)
val comment = ProductComment(productId = productId, memberId = memberId, content = content)
return productCommentRepository.save(comment)
}

fun update(productId: Long, memberId: Long, content: String): ProductComment {
inactivate(productId, memberId)
val newComment = ProductComment(productId = productId, memberId = memberId, content = content)
return productCommentRepository.save(newComment)
}

fun activate(productId: Long, memberId: Long) {
productCommentRepository.findLatestByProductIdAndMemberId(productId, memberId)?.apply { if(!valid) valid = true }
}

fun inactivate(productId: Long, memberId: Long) {
productCommentRepository.findLatestByProductIdAndMemberId(productId, memberId)?.apply { if(valid) valid = false }
?: throw NotFoundSourceException("productId: $productId 에 대한 코멘트가 존재하지 않습니다.")
}

fun inactivateIfExist(productId: Long, memberId: Long) {
productCommentRepository.findLatestByProductIdAndMemberId(productId, memberId)?.apply { if(valid) valid = false }
}

private fun validateCommentDuplication(productId: Long, memberId: Long) {
if (productCommentRepository.existsByProductIdAndMemberIdAndValidTrue(productId, memberId)) {
throw BadRequestException("productId: $productId 에 대한 코멘트가 이미 존재합니다.")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.yapp.cvs.domain.comment.entity

import com.yapp.cvs.domain.base.BaseEntity
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
import javax.persistence.Table

@Entity
@Table(name = "product_comments")
class ProductComment(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val productCommentId: Long? = null,

val productId: Long,

val memberId: Long,

var content: String,

var valid: Boolean = true,
): BaseEntity()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.yapp.cvs.domain.comment.entity

enum class ProductCommentOrderType {
RECENT,
LIKE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.yapp.cvs.domain.comment.repository

import com.yapp.cvs.domain.comment.entity.ProductComment
import com.yapp.cvs.domain.comment.vo.ProductCommentDetailVO
import com.yapp.cvs.domain.comment.vo.ProductCommentSearchVO
import org.springframework.data.jpa.repository.JpaRepository

interface ProductCommentRepository : JpaRepository<ProductComment, Long>, ProductCommentCustom {
fun findByProductIdAndMemberIdAndValidTrue(productId: Long, memberId: Long): ProductComment?
fun existsByProductIdAndMemberIdAndValidTrue(productId: Long, memberId: Long): Boolean
fun deleteByProductIdAndMemberId(productId: Long, memberId: Long)
}

interface ProductCommentCustom {
fun findLatestByProductIdAndMemberId(productId: Long, memberId: Long): ProductComment?
fun findAllByProductIdAndPageOffset(productId: Long, productCommentSearchVO: ProductCommentSearchVO): List<ProductCommentDetailVO>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.yapp.cvs.domain.comment.repository.impl

import com.querydsl.core.types.ConstructorExpression
import com.querydsl.core.types.OrderSpecifier
import com.querydsl.core.types.Projections
import com.querydsl.core.types.dsl.Expressions
import com.yapp.cvs.domain.comment.entity.ProductComment
import com.yapp.cvs.domain.comment.entity.ProductCommentOrderType
import com.yapp.cvs.domain.comment.entity.QProductComment.productComment
import com.yapp.cvs.domain.comment.repository.ProductCommentCustom
import com.yapp.cvs.domain.comment.vo.ProductCommentDetailVO
import com.yapp.cvs.domain.comment.vo.ProductCommentSearchVO
import com.yapp.cvs.domain.like.entity.QMemberProductLikeMapping.memberProductLikeMapping
import com.yapp.cvs.domain.member.entity.QMember.member
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport
import org.springframework.stereotype.Repository

@Repository
class ProductCommentRepositoryImpl: QuerydslRepositorySupport(ProductComment::class.java), ProductCommentCustom {
override fun findLatestByProductIdAndMemberId(productId: Long, memberId: Long): ProductComment? {
return from(productComment)
.where(
productComment.productId.eq(productId),
productComment.memberId.eq(memberId),
)
.orderBy(productComment.productCommentId.desc())
.fetchFirst()
}

override fun findAllByProductIdAndPageOffset(productId: Long,
productCommentSearchVO: ProductCommentSearchVO): List<ProductCommentDetailVO> {
val size = productCommentSearchVO.pageSize
val offsetId = productCommentSearchVO.offsetProductCommentId
var predicate = productComment.productId.eq(productId)
.and(productComment.valid.isTrue)
if (offsetId != null) {
predicate = predicate.and(productComment.productCommentId.lt(offsetId))
}

return from(productComment)
.leftJoin(member)
.on(productComment.memberId.eq(member.memberId))
.leftJoin(memberProductLikeMapping)
.on(
productComment.productId.eq(memberProductLikeMapping.productId),
productComment.memberId.eq(memberProductLikeMapping.memberId)
)
.where(predicate)
.orderBy(getOrderBy(productCommentSearchVO.orderBy))
.select(productDetailVOProjection())
.limit(size)
.fetch()
}

private fun productDetailVOProjection(): ConstructorExpression<ProductCommentDetailVO>? {
// TODO : commentLikeCount 입력
val tempCommentLikeCount = 10L

return Projections.constructor(
ProductCommentDetailVO::class.java,
productComment.productCommentId,
productComment.content,
Expressions.asNumber(tempCommentLikeCount),
productComment.createdAt,
memberProductLikeMapping.likeType,
productComment.productId,
productComment.memberId,
member.nickName,
Expressions.FALSE
)
}

private fun getOrderBy(productCommentOrderType: ProductCommentOrderType): OrderSpecifier<*> {
return if (productCommentOrderType == ProductCommentOrderType.RECENT) {
productComment.productCommentId.desc()
} else {
productComment.createdAt.desc()
}
}
}
Loading

0 comments on commit dfa3354

Please sign in to comment.