diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt index 4164c93..a62b247 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt @@ -82,6 +82,8 @@ class AdminAudioContentQueryRepositoryImpl( audioContentTheme.theme, audioContentTheme.id, audioContent.price, + audioContent.limited, + audioContent.remaining, audioContent.isAdult, audioContent.duration, audioContent.content, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt index 40f974a..1fe1ba7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt @@ -18,6 +18,8 @@ data class GetAdminContentListItem @QueryProjection constructor( val theme: String, val themeId: Long, val price: Int, + val totalContentCount: Int?, + val remainingContentCount: Int?, val isAdult: Boolean, val remainingTime: String, var contentUrl: String, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt index 44acb16..8536827 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt @@ -34,6 +34,8 @@ data class AudioContent( var detail: String, val price: Int = 0, var releaseDate: LocalDateTime? = null, + val limited: Int? = null, + var remaining: Int? = null, @Enumerated(value = EnumType.STRING) val type: AudioContentType = AudioContentType.INDIVIDUAL, val isGeneratePreview: Boolean = true, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index de0e173..86f5a31 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -14,6 +14,7 @@ import kr.co.vividnext.sodalive.content.like.AudioContentLikeRepository import kr.co.vividnext.sodalive.content.like.PutAudioContentLikeRequest import kr.co.vividnext.sodalive.content.like.PutAudioContentLikeResponse import kr.co.vividnext.sodalive.content.main.GetAudioContentRanking +import kr.co.vividnext.sodalive.content.order.LimitedEditionOrderRepository import kr.co.vividnext.sodalive.content.order.OrderRepository import kr.co.vividnext.sodalive.content.order.OrderType import kr.co.vividnext.sodalive.content.pin.PinContent @@ -47,6 +48,7 @@ class AudioContentService( private val blockMemberRepository: BlockMemberRepository, private val hashTagRepository: HashTagRepository, private val orderRepository: OrderRepository, + private val limitedEditionOrderRepository: LimitedEditionOrderRepository, private val themeQueryRepository: AudioContentThemeQueryRepository, private val playbackTrackingRepository: PlaybackTrackingRepository, private val commentRepository: AudioContentCommentRepository, @@ -183,13 +185,15 @@ class AudioContentService( 0 }, releaseDate = releaseDate, + limited = request.limited, + remaining = request.limited, isAdult = request.isAdult, isGeneratePreview = if (request.type == AudioContentType.INDIVIDUAL) { request.isGeneratePreview } else { false }, - isOnlyRental = request.isOnlyRental, + isOnlyRental = if (request.limited != null && request.limited > 0) false else request.isOnlyRental, isCommentAvailable = request.isCommentAvailable ) audioContent.theme = theme @@ -463,11 +467,21 @@ class AudioContentService( contentId = audioContent.id!! ) + val existOrdered = isExistsBundleAudioContent || isExistsAudioContent + val orderSequence = if (existOrdered) { + limitedEditionOrderRepository.getOrderSequence( + contentId = audioContent.id!!, + memberId = member.id!! + ) + } else { + null + } + if ( - !isExistsAudioContent && - !isExistsBundleAudioContent && + !existOrdered && !audioContent.isActive && - audioContent.releaseDate == null + audioContent.releaseDate != null && + audioContent.releaseDate!! < LocalDateTime.now() ) { throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") } @@ -591,11 +605,14 @@ class AudioContentService( price = audioContent.price, duration = audioContent.duration ?: "", releaseDate = releaseDate, + totalContentCount = audioContent.limited, + remainingContentCount = audioContent.remaining, + orderSequence = orderSequence, isActivePreview = audioContent.isGeneratePreview, isAdult = audioContent.isAdult, isMosaic = audioContent.isAdult && member.auth == null, isOnlyRental = audioContent.isOnlyRental, - existOrdered = isExistsBundleAudioContent || isExistsAudioContent, + existOrdered = existOrdered, orderType = orderType, remainingTime = remainingTime, creatorOtherContentList = creatorOtherContentList, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt index 2ebb4b5..7a155c1 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt @@ -5,6 +5,7 @@ data class CreateAudioContentRequest( val detail: String, val tags: String, val price: Int, + val limited: Int? = null, val timezone: String = "Asia/Seoul", val releaseDate: String? = null, val themeId: Long = 0, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt index 0049565..77e5ea7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt @@ -15,6 +15,9 @@ data class GetAudioContentDetailResponse( val price: Int, val duration: String, val releaseDate: String?, + val totalContentCount: Int?, + val remainingContentCount: Int?, + val orderSequence: Int?, val isActivePreview: Boolean, val isAdult: Boolean, val isMosaic: Boolean, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/LimitedEditionOrder.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/LimitedEditionOrder.kt new file mode 100644 index 0000000..766f82c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/LimitedEditionOrder.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.content.order + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.content.AudioContent +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToOne + +@Entity +data class LimitedEditionOrder( + val sequence: Int +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id", nullable = false) + var audioContent: AudioContent? = null + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_id", nullable = false) + var order: Order? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/LimitedEditionOrderRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/LimitedEditionOrderRepository.kt new file mode 100644 index 0000000..a97cf49 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/LimitedEditionOrderRepository.kt @@ -0,0 +1,49 @@ +package kr.co.vividnext.sodalive.content.order + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.QAudioContent.audioContent +import kr.co.vividnext.sodalive.content.order.QLimitedEditionOrder.limitedEditionOrder +import kr.co.vividnext.sodalive.content.order.QOrder.order +import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.data.jpa.repository.JpaRepository + +interface LimitedEditionOrderRepository : JpaRepository, LimitedEditionOrderQueryRepository + +interface LimitedEditionOrderQueryRepository { + fun getNextSequence(contentId: Long): Int + + fun getOrderSequence(contentId: Long, memberId: Long): Int? +} + +class LimitedEditionOrderQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : LimitedEditionOrderQueryRepository { + override fun getNextSequence(contentId: Long): Int { + val maxSequence = queryFactory.select(limitedEditionOrder.sequence) + .from(limitedEditionOrder) + .innerJoin(limitedEditionOrder.audioContent, audioContent) + .where(limitedEditionOrder.audioContent.id.eq(contentId)) + .orderBy(limitedEditionOrder.sequence.desc()) + .limit(1) + .fetchFirst() + + return if (maxSequence == null) { + 1 + } else { + maxSequence + 1 + } + } + + override fun getOrderSequence(contentId: Long, memberId: Long): Int? { + return queryFactory.select(limitedEditionOrder.sequence) + .from(limitedEditionOrder) + .innerJoin(limitedEditionOrder.audioContent, audioContent) + .innerJoin(limitedEditionOrder.order, order) + .innerJoin(order.member, member) + .where( + limitedEditionOrder.audioContent.id.eq(contentId) + .and(limitedEditionOrder.order.member.id.eq(memberId)) + ) + .fetchFirst() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt index 0ca2b85..06cba52 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderService.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.content.order import kr.co.vividnext.sodalive.can.payment.CanPaymentService import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.AudioContent import kr.co.vividnext.sodalive.content.AudioContentRepository import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository import kr.co.vividnext.sodalive.content.like.AudioContentLikeRepository @@ -21,6 +22,7 @@ class OrderService( private val audioContentRepository: AudioContentRepository, private val audioContentCommentQueryRepository: AudioContentCommentRepository, private val audioContentLikeQueryRepository: AudioContentLikeRepository, + private val limitedEditionOrderRepository: LimitedEditionOrderRepository, @Value("\${cloud.aws.cloud-front.host}") private val audioContentCoverImageHost: String @@ -29,24 +31,15 @@ class OrderService( fun order(contentId: Long, orderType: OrderType, container: String, member: Member) { val content = audioContentRepository.findByIdAndActive(contentId) ?: throw SodaException("잘못된 콘텐츠 입니다\n다시 시도해 주세요.") + validateOrder(memberId = member.id!!, content = content) - val order = if (content.isOnlyRental) { - Order(type = OrderType.RENTAL) + val order = if (content.limited != null && content.remaining != null) { + if (content.remaining!! <= 0) throw SodaException("해당 콘텐츠가 매진되었습니다.") + orderLimitedEditionContent(content, member) } else { - Order(type = orderType) + orderContent(orderType, content, member) } - if (member.id!! == content.member!!.id!!) throw SodaException("자신이 올린 콘텐츠는 구매할 수 없습니다.") - if (repository.isExistOrdered(memberId = member.id!!, contentId = contentId)) { - throw SodaException("이미 구매한 콘텐츠 입니다.") - } - - order.member = member - order.creator = content.member - order.audioContent = content - - repository.save(order) - canPaymentService.spendCan( memberId = member.id!!, needCan = order.can, @@ -56,6 +49,39 @@ class OrderService( ) } + private fun orderContent(orderType: OrderType, content: AudioContent, member: Member): Order { + val order = if (content.isOnlyRental) { + Order(type = OrderType.RENTAL) + } else { + Order(type = orderType) + } + order.member = member + order.creator = content.member + order.audioContent = content + return repository.save(order) + } + + private fun orderLimitedEditionContent(content: AudioContent, member: Member): Order { + val order = orderContent(OrderType.KEEP, content, member) + + val sequence = limitedEditionOrderRepository.getNextSequence(content.id!!) + val limitedEditionOrder = LimitedEditionOrder(sequence = sequence) + limitedEditionOrder.order = order + limitedEditionOrder.audioContent = content + limitedEditionOrderRepository.save(limitedEditionOrder) + + content.remaining = content.remaining!! - 1 + + return order + } + + private fun validateOrder(memberId: Long, content: AudioContent) { + if (memberId == content.member!!.id!!) throw SodaException("자신이 올린 콘텐츠는 구매할 수 없습니다.") + if (repository.isExistOrdered(memberId = memberId, contentId = content.id!!)) { + throw SodaException("이미 구매한 콘텐츠 입니다.") + } + } + fun getAudioContentOrderList( member: Member, offset: Long, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/CreatorAdminContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/CreatorAdminContentRepository.kt index e856992..4a0a3da 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/CreatorAdminContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/CreatorAdminContentRepository.kt @@ -81,6 +81,8 @@ class CreatorAdminAudioContentQueryRepositoryImpl( audioContent.member!!.nickname, audioContentTheme.theme, audioContent.price, + audioContent.limited, + audioContent.remaining, audioContent.isAdult, audioContent.isCommentAvailable, audioContent.duration, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/GetCreatorAdminContentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/GetCreatorAdminContentListResponse.kt index 5467fd3..5176950 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/GetCreatorAdminContentListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/GetCreatorAdminContentListResponse.kt @@ -15,6 +15,8 @@ data class GetCreatorAdminContentListItem @QueryProjection constructor( val creatorNickname: String, val theme: String, val price: Int, + val totalContentCount: Int?, + val remainingContentCount: Int?, val isAdult: Boolean, val isCommentAvailable: Boolean, val remainingTime: String,