콘텐츠 API 추가
This commit is contained in:
parent
5d6eb5da4f
commit
1fe5309fdc
|
@ -0,0 +1,48 @@
|
|||
package kr.co.vividnext.sodalive.aws.cloudfront
|
||||
|
||||
import com.amazonaws.services.cloudfront.CloudFrontUrlSigner
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Component
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyFactory
|
||||
import java.security.PrivateKey
|
||||
import java.security.spec.PKCS8EncodedKeySpec
|
||||
import java.util.Date
|
||||
|
||||
@Component
|
||||
class AudioContentCloudFront(
|
||||
@Value("\${cloud.aws.content-cloud-front.host}")
|
||||
private val cloudfrontDomain: String,
|
||||
|
||||
@Value("\${cloud.aws.content-cloud-front.private-key-file-path}")
|
||||
private val privateKeyFilePath: String,
|
||||
|
||||
@Value("\${cloud.aws.content-cloud-front.key-pair-id}")
|
||||
private val keyPairId: String
|
||||
) {
|
||||
fun generateSignedURL(
|
||||
resourcePath: String,
|
||||
expirationTime: Long
|
||||
): String {
|
||||
// Load private key from file
|
||||
val privateKey = loadPrivateKey(privateKeyFilePath)
|
||||
|
||||
// Generate signed URL for resource with custom policy and expiration time
|
||||
|
||||
return CloudFrontUrlSigner.getSignedURLWithCannedPolicy(
|
||||
"https://$cloudfrontDomain/$resourcePath", // Resource URL
|
||||
keyPairId, // CloudFront key pair ID
|
||||
privateKey, // CloudFront private key
|
||||
Date(System.currentTimeMillis() + expirationTime) // Expiration date
|
||||
)
|
||||
}
|
||||
|
||||
private fun loadPrivateKey(resourceName: String): PrivateKey {
|
||||
val path = Paths.get(resourceName)
|
||||
val bytes = Files.readAllBytes(path)
|
||||
val keySpec = PKCS8EncodedKeySpec(bytes)
|
||||
val keyFactory = KeyFactory.getInstance("RSA")
|
||||
return keyFactory.generatePrivate(keySpec)
|
||||
}
|
||||
}
|
|
@ -13,6 +13,8 @@ import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
|
|||
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
|
||||
import kr.co.vividnext.sodalive.can.use.UseCanRepository
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.order.Order
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoom
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
|
@ -33,6 +35,8 @@ class CanPaymentService(
|
|||
needCan: Int,
|
||||
canUsage: CanUsage,
|
||||
liveRoom: LiveRoom? = null,
|
||||
order: Order? = null,
|
||||
audioContent: AudioContent? = null,
|
||||
container: String
|
||||
) {
|
||||
val member = memberRepository.findByIdOrNull(id = memberId)
|
||||
|
@ -72,6 +76,14 @@ class CanPaymentService(
|
|||
recipientId = liveRoom.member!!.id!!
|
||||
useCan.room = liveRoom
|
||||
useCan.member = member
|
||||
} else if (canUsage == CanUsage.ORDER_CONTENT && order != null) {
|
||||
recipientId = order.creator!!.id!!
|
||||
useCan.order = order
|
||||
useCan.member = member
|
||||
} else if (canUsage == CanUsage.DONATION && audioContent != null) {
|
||||
recipientId = audioContent.member!!.id!!
|
||||
useCan.audioContent = audioContent
|
||||
useCan.member = member
|
||||
} else {
|
||||
throw SodaException("잘못된 요청입니다.")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package kr.co.vividnext.sodalive.can.use
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.order.Order
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoom
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import javax.persistence.CascadeType
|
||||
|
@ -11,6 +13,7 @@ import javax.persistence.FetchType
|
|||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.OneToMany
|
||||
import javax.persistence.OneToOne
|
||||
|
||||
@Entity
|
||||
data class UseCan(
|
||||
|
@ -35,6 +38,14 @@ data class UseCan(
|
|||
field = value
|
||||
}
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "order_id", nullable = true)
|
||||
var order: Order? = null
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "content_id", nullable = true)
|
||||
var audioContent: AudioContent? = null
|
||||
|
||||
@OneToMany(mappedBy = "useCan", cascade = [CascadeType.ALL])
|
||||
val useCanCalculates: MutableList<UseCanCalculate> = mutableListOf()
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
data class AddAllPlaybackTrackingRequest(
|
||||
val timezone: String,
|
||||
val trackingDataList: List<PlaybackTrackingData>
|
||||
)
|
||||
|
||||
data class PlaybackTrackingData(
|
||||
val contentId: Long,
|
||||
val playDateTime: String,
|
||||
val isPreview: Boolean
|
||||
)
|
|
@ -0,0 +1,64 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import kr.co.vividnext.sodalive.content.hashtag.AudioContentHashTag
|
||||
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import javax.persistence.CascadeType
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
import javax.persistence.Enumerated
|
||||
import javax.persistence.FetchType
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.OneToMany
|
||||
import javax.persistence.OneToOne
|
||||
import javax.persistence.Table
|
||||
|
||||
enum class AudioContentType {
|
||||
INDIVIDUAL, BUNDLE
|
||||
}
|
||||
|
||||
enum class SortType {
|
||||
NEWEST, PRICE_HIGH, PRICE_LOW
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "content")
|
||||
data class AudioContent(
|
||||
var title: String,
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
var detail: String,
|
||||
val price: Int = 0,
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
val type: AudioContentType = AudioContentType.INDIVIDUAL,
|
||||
val isGeneratePreview: Boolean = true,
|
||||
var isAdult: Boolean = false,
|
||||
var isCommentAvailable: Boolean = true
|
||||
) : BaseEntity() {
|
||||
var isActive: Boolean = false
|
||||
var content: String? = null
|
||||
var coverImage: String? = null
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "theme_id", nullable = false)
|
||||
var theme: AudioContentTheme? = null
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "curation_id", nullable = true)
|
||||
var curation: AudioContentCuration? = null
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", nullable = false)
|
||||
var member: Member? = null
|
||||
|
||||
var duration: String? = null
|
||||
|
||||
@OneToMany(mappedBy = "audioContent", cascade = [CascadeType.ALL])
|
||||
val audioContentHashTags: MutableList<AudioContentHashTag> = mutableListOf()
|
||||
|
||||
@OneToMany(mappedBy = "child", cascade = [CascadeType.ALL])
|
||||
var children: MutableList<BundleAudioContent> = mutableListOf()
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.content.like.PutAudioContentLikeRequest
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.lang.Nullable
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
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.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RequestPart
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/audio-content")
|
||||
class AudioContentController(private val service: AudioContentService) {
|
||||
@PostMapping
|
||||
@PreAuthorize("hasRole('CREATOR')")
|
||||
fun createAudioContent(
|
||||
@Nullable
|
||||
@RequestPart("contentFile")
|
||||
contentFile: MultipartFile?,
|
||||
@RequestPart("coverImage") coverImage: MultipartFile?,
|
||||
@RequestPart("request") requestString: String,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(
|
||||
service.createAudioContent(
|
||||
contentFile = contentFile,
|
||||
coverImage = coverImage,
|
||||
requestString = requestString,
|
||||
member = member
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorize("hasRole('CREATOR')")
|
||||
fun modifyAudioContent(
|
||||
@Nullable
|
||||
@RequestPart("coverImage")
|
||||
coverImage: MultipartFile?,
|
||||
@RequestPart("request") requestString: String,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(
|
||||
service.modifyAudioContent(
|
||||
coverImage = coverImage,
|
||||
requestString = requestString,
|
||||
member = member
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@PutMapping("/upload-complete")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
fun uploadComplete(
|
||||
@RequestBody request: UploadCompleteRequest,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(
|
||||
service.uploadComplete(
|
||||
contentId = request.contentId,
|
||||
content = request.contentPath,
|
||||
duration = request.duration
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasRole('CREATOR')")
|
||||
fun deleteAudioContent(
|
||||
@PathVariable id: Long,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(
|
||||
service.deleteAudioContent(
|
||||
audioContentId = id,
|
||||
member = member
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
fun getAudioContentList(
|
||||
@RequestParam("creator-id") creatorId: Long,
|
||||
@RequestParam("sort-type", required = false) sortType: SortType = SortType.NEWEST,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
pageable: Pageable
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(
|
||||
service.getAudioContentList(
|
||||
creatorId = creatorId,
|
||||
sortType = sortType,
|
||||
member = member,
|
||||
offset = pageable.offset,
|
||||
limit = pageable.pageSize.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
fun getDetail(
|
||||
@PathVariable id: Long,
|
||||
@RequestParam timezone: String,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(service.getDetail(id = id, member = member, timezone = timezone))
|
||||
}
|
||||
|
||||
@PostMapping("/playback-tracking")
|
||||
fun addAllPlaybackTracking(
|
||||
@RequestBody request: AddAllPlaybackTrackingRequest,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(service.addAllPlaybackTracking(request, member))
|
||||
}
|
||||
|
||||
@PutMapping("/like")
|
||||
fun audioContentLike(
|
||||
@RequestBody request: PutAudioContentLikeRequest,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(service.audioContentLike(request, member))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,341 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import com.querydsl.core.types.dsl.Expressions
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||
import kr.co.vividnext.sodalive.content.QBundleAudioContent.bundleAudioContent
|
||||
import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem
|
||||
import kr.co.vividnext.sodalive.content.main.GetNewContentUploadCreator
|
||||
import kr.co.vividnext.sodalive.content.main.QGetAudioContentMainItem
|
||||
import kr.co.vividnext.sodalive.content.main.QGetNewContentUploadCreator
|
||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner
|
||||
import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioContentBanner
|
||||
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
|
||||
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration
|
||||
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
|
||||
import kr.co.vividnext.sodalive.event.QEvent.event
|
||||
import kr.co.vividnext.sodalive.member.QMember.member
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
interface AudioContentRepository : JpaRepository<AudioContent, Long>, AudioContentQueryRepository
|
||||
|
||||
interface AudioContentQueryRepository {
|
||||
fun findByIdAndActive(contentId: Long): AudioContent?
|
||||
fun findByIdAndCreatorId(contentId: Long, creatorId: Long): AudioContent?
|
||||
fun findBundleByContentId(contentId: Long): List<AudioContent>
|
||||
fun findByCreatorId(
|
||||
creatorId: Long,
|
||||
isAdult: Boolean = false,
|
||||
sortType: SortType = SortType.NEWEST,
|
||||
offset: Long = 0,
|
||||
limit: Long = 10
|
||||
): List<AudioContent>
|
||||
|
||||
fun findTotalCountByCreatorId(creatorId: Long, isAdult: Boolean = false): Int
|
||||
fun getCreatorOtherContentList(
|
||||
cloudfrontHost: String,
|
||||
contentId: Long,
|
||||
creatorId: Long,
|
||||
isAdult: Boolean
|
||||
): List<OtherContentResponse>
|
||||
|
||||
fun getSameThemeOtherContentList(
|
||||
cloudfrontHost: String,
|
||||
contentId: Long,
|
||||
themeId: Long,
|
||||
isAdult: Boolean
|
||||
): List<OtherContentResponse>
|
||||
|
||||
fun findByTheme(
|
||||
cloudfrontHost: String,
|
||||
theme: String = "",
|
||||
isAdult: Boolean = false,
|
||||
limit: Long = 20
|
||||
): List<GetAudioContentMainItem>
|
||||
|
||||
fun getNewContentUploadCreatorList(
|
||||
cloudfrontHost: String,
|
||||
isAdult: Boolean = false
|
||||
): List<GetNewContentUploadCreator>
|
||||
|
||||
fun getAudioContentMainBannerList(isAdult: Boolean): List<AudioContentBanner>
|
||||
fun getAudioContentCurations(isAdult: Boolean): List<AudioContentCuration>
|
||||
fun findAudioContentByCurationId(
|
||||
curationId: Long,
|
||||
cloudfrontHost: String,
|
||||
isAdult: Boolean
|
||||
): List<GetAudioContentMainItem>
|
||||
}
|
||||
|
||||
@Repository
|
||||
class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AudioContentQueryRepository {
|
||||
override fun findByIdAndActive(contentId: Long): AudioContent? {
|
||||
return queryFactory
|
||||
.selectFrom(audioContent)
|
||||
.where(
|
||||
audioContent.isActive.isTrue
|
||||
.and(audioContent.id.eq(contentId))
|
||||
)
|
||||
.fetchOne()
|
||||
}
|
||||
|
||||
override fun findByIdAndCreatorId(contentId: Long, creatorId: Long): AudioContent? {
|
||||
return queryFactory
|
||||
.selectFrom(audioContent)
|
||||
.where(
|
||||
audioContent.isActive.isTrue
|
||||
.and(audioContent.id.eq(contentId))
|
||||
.and(audioContent.member.id.eq(creatorId))
|
||||
)
|
||||
.fetchOne()
|
||||
}
|
||||
|
||||
// 해당 컨텐츠가 속한 묶음(번들) 상품 리스트 검색
|
||||
override fun findBundleByContentId(contentId: Long): List<AudioContent> {
|
||||
return queryFactory
|
||||
.select(bundleAudioContent.parent)
|
||||
.from(bundleAudioContent)
|
||||
.where(
|
||||
bundleAudioContent.child.id.eq(contentId)
|
||||
.and(bundleAudioContent.child.isActive.isTrue)
|
||||
)
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findByCreatorId(
|
||||
creatorId: Long,
|
||||
isAdult: Boolean,
|
||||
sortType: SortType,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<AudioContent> {
|
||||
val orderBy = when (sortType) {
|
||||
SortType.NEWEST -> audioContent.createdAt.desc()
|
||||
SortType.PRICE_HIGH -> audioContent.price.desc()
|
||||
SortType.PRICE_LOW -> audioContent.price.asc()
|
||||
}
|
||||
|
||||
var where = audioContent.isActive.isTrue
|
||||
.and(audioContent.member.id.eq(creatorId))
|
||||
|
||||
if (!isAdult) {
|
||||
where = where.and(audioContent.isAdult.isFalse)
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.selectFrom(audioContent)
|
||||
.where(where)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.orderBy(orderBy)
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findTotalCountByCreatorId(
|
||||
creatorId: Long,
|
||||
isAdult: Boolean
|
||||
): Int {
|
||||
var where = audioContent.isActive.isTrue
|
||||
.and(audioContent.member.id.eq(creatorId))
|
||||
|
||||
if (!isAdult) {
|
||||
where = where.and(audioContent.isAdult.isFalse)
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.selectFrom(audioContent)
|
||||
.where(where)
|
||||
.fetch()
|
||||
.size
|
||||
}
|
||||
|
||||
override fun getCreatorOtherContentList(
|
||||
cloudfrontHost: String,
|
||||
contentId: Long,
|
||||
creatorId: Long,
|
||||
isAdult: Boolean
|
||||
): List<OtherContentResponse> {
|
||||
var where = audioContent.id.ne(contentId)
|
||||
.and(audioContent.member.id.eq(creatorId))
|
||||
.and(audioContent.isActive.isTrue)
|
||||
|
||||
if (!isAdult) {
|
||||
where = where.and(audioContent.isAdult.isFalse)
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
QOtherContentResponse(
|
||||
audioContent.id,
|
||||
audioContent.title,
|
||||
audioContent.coverImage.prepend("$cloudfrontHost/")
|
||||
)
|
||||
)
|
||||
.from(audioContent)
|
||||
.where(where)
|
||||
.offset(0)
|
||||
.limit(10)
|
||||
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun getSameThemeOtherContentList(
|
||||
cloudfrontHost: String,
|
||||
contentId: Long,
|
||||
themeId: Long,
|
||||
isAdult: Boolean
|
||||
): List<OtherContentResponse> {
|
||||
var where = audioContent.id.ne(contentId)
|
||||
.and(audioContent.theme.id.eq(themeId))
|
||||
.and(audioContent.isActive.isTrue)
|
||||
|
||||
if (!isAdult) {
|
||||
where = where.and(audioContent.isAdult.isFalse)
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
QOtherContentResponse(
|
||||
audioContent.id,
|
||||
audioContent.title,
|
||||
audioContent.coverImage.prepend("$cloudfrontHost/")
|
||||
)
|
||||
)
|
||||
.from(audioContent)
|
||||
.where(where)
|
||||
.offset(0)
|
||||
.limit(10)
|
||||
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findByTheme(
|
||||
cloudfrontHost: String,
|
||||
theme: String,
|
||||
isAdult: Boolean,
|
||||
limit: Long
|
||||
): List<GetAudioContentMainItem> {
|
||||
var where = audioContent.isActive.isTrue
|
||||
|
||||
if (!isAdult) {
|
||||
where = where.and(audioContent.isAdult.isFalse)
|
||||
}
|
||||
|
||||
if (theme.isNotBlank()) {
|
||||
where = where.and(audioContentTheme.theme.eq(theme))
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetAudioContentMainItem(
|
||||
audioContent.id,
|
||||
audioContent.coverImage.prepend("/").prepend(cloudfrontHost),
|
||||
audioContent.title,
|
||||
audioContent.isAdult,
|
||||
member.id,
|
||||
member.profileImage.prepend("/").prepend(cloudfrontHost),
|
||||
member.nickname
|
||||
)
|
||||
)
|
||||
.from(audioContent)
|
||||
.innerJoin(audioContent.member, member)
|
||||
.innerJoin(audioContent.theme, audioContentTheme)
|
||||
.where(where)
|
||||
.limit(limit)
|
||||
.orderBy(audioContent.createdAt.desc())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun getNewContentUploadCreatorList(
|
||||
cloudfrontHost: String,
|
||||
isAdult: Boolean
|
||||
): List<GetNewContentUploadCreator> {
|
||||
var where = audioContent.createdAt.after(LocalDateTime.now().minusWeeks(2))
|
||||
.and(audioContent.isActive.isTrue)
|
||||
|
||||
if (!isAdult) {
|
||||
where = where.and(audioContent.isAdult.isFalse)
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetNewContentUploadCreator(
|
||||
member.id,
|
||||
member.nickname,
|
||||
member.profileImage.nullif("profile/default-profile.png").prepend("$cloudfrontHost/")
|
||||
)
|
||||
)
|
||||
.from(audioContent)
|
||||
.innerJoin(audioContent.member, member)
|
||||
.where(where)
|
||||
.groupBy(member.id)
|
||||
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
|
||||
.limit(20)
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun getAudioContentMainBannerList(isAdult: Boolean): List<AudioContentBanner> {
|
||||
var where = audioContentBanner.isActive.isTrue
|
||||
|
||||
if (!isAdult) {
|
||||
where = where.and(audioContentBanner.isAdult.isFalse)
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.selectFrom(audioContentBanner)
|
||||
.leftJoin(audioContentBanner.event, event)
|
||||
.leftJoin(audioContentBanner.creator, member)
|
||||
.where(where)
|
||||
.orderBy(audioContentBanner.orders.asc())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun getAudioContentCurations(isAdult: Boolean): List<AudioContentCuration> {
|
||||
var where = audioContentCuration.isActive.isTrue
|
||||
|
||||
if (!isAdult) {
|
||||
where = where.and(audioContentCuration.isAdult.isFalse)
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.selectFrom(audioContentCuration)
|
||||
.where(where)
|
||||
.orderBy(audioContentCuration.orders.asc())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findAudioContentByCurationId(
|
||||
curationId: Long,
|
||||
cloudfrontHost: String,
|
||||
isAdult: Boolean
|
||||
): List<GetAudioContentMainItem> {
|
||||
var where = audioContent.isActive.isTrue
|
||||
.and(audioContent.curation.id.eq(curationId))
|
||||
|
||||
if (!isAdult) {
|
||||
where = where.and(audioContent.isAdult.isFalse)
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetAudioContentMainItem(
|
||||
audioContent.id,
|
||||
audioContent.coverImage.prepend("/").prepend(cloudfrontHost),
|
||||
audioContent.title,
|
||||
audioContent.isAdult,
|
||||
member.id,
|
||||
member.profileImage.nullif("profile/default-profile.png")
|
||||
.prepend("/")
|
||||
.prepend(cloudfrontHost),
|
||||
member.nickname
|
||||
)
|
||||
)
|
||||
.from(audioContent)
|
||||
.where(where)
|
||||
.orderBy(audioContent.id.desc())
|
||||
.fetch()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,469 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import com.amazonaws.services.s3.model.ObjectMetadata
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
|
||||
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
||||
import kr.co.vividnext.sodalive.content.hashtag.AudioContentHashTag
|
||||
import kr.co.vividnext.sodalive.content.hashtag.HashTag
|
||||
import kr.co.vividnext.sodalive.content.hashtag.HashTagRepository
|
||||
import kr.co.vividnext.sodalive.content.like.AudioContentLike
|
||||
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.order.OrderRepository
|
||||
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
|
||||
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.utils.generateFileName
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
class AudioContentService(
|
||||
private val repository: AudioContentRepository,
|
||||
private val explorerQueryRepository: ExplorerQueryRepository,
|
||||
private val blockMemberRepository: BlockMemberRepository,
|
||||
private val hashTagRepository: HashTagRepository,
|
||||
private val orderRepository: OrderRepository,
|
||||
private val themeQueryRepository: AudioContentThemeQueryRepository,
|
||||
private val playbackTrackingRepository: PlaybackTrackingRepository,
|
||||
private val commentRepository: AudioContentCommentRepository,
|
||||
private val audioContentLikeRepository: AudioContentLikeRepository,
|
||||
|
||||
private val s3Uploader: S3Uploader,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val audioContentCloudFront: AudioContentCloudFront,
|
||||
|
||||
@Value("\${cloud.aws.s3.content-bucket}")
|
||||
private val audioContentBucket: String,
|
||||
|
||||
@Value("\${cloud.aws.s3.bucket}")
|
||||
private val coverImageBucket: String,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val coverImageHost: String
|
||||
) {
|
||||
@Transactional
|
||||
fun audioContentLike(request: PutAudioContentLikeRequest, member: Member): PutAudioContentLikeResponse {
|
||||
var audioContentLike = audioContentLikeRepository.findByMemberIdAndContentId(
|
||||
memberId = member.id!!,
|
||||
contentId = request.contentId
|
||||
)
|
||||
|
||||
if (audioContentLike == null) {
|
||||
audioContentLike = AudioContentLike(
|
||||
memberId = member.id!!,
|
||||
contentId = request.contentId
|
||||
)
|
||||
|
||||
audioContentLikeRepository.save(audioContentLike)
|
||||
} else {
|
||||
audioContentLike.isActive = !audioContentLike.isActive
|
||||
}
|
||||
|
||||
return PutAudioContentLikeResponse(like = audioContentLike.isActive)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun modifyAudioContent(
|
||||
coverImage: MultipartFile?,
|
||||
requestString: String,
|
||||
member: Member
|
||||
) {
|
||||
// request 내용 파싱
|
||||
val request = objectMapper.readValue(requestString, ModifyAudioContentRequest::class.java)
|
||||
|
||||
val audioContent = repository.findByIdAndCreatorId(request.contentId, member.id!!)
|
||||
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
|
||||
|
||||
if (request.title != null) audioContent.title = request.title
|
||||
if (request.detail != null) audioContent.detail = request.detail
|
||||
audioContent.isCommentAvailable = request.isCommentAvailable
|
||||
audioContent.isAdult = request.isAdult
|
||||
|
||||
if (coverImage != null) {
|
||||
val metadata = ObjectMetadata()
|
||||
metadata.contentLength = coverImage.size
|
||||
|
||||
// 커버 이미지 파일명 생성
|
||||
val coverImageFileName = generateFileName(prefix = "${audioContent.id}-cover")
|
||||
|
||||
// 커버 이미지 업로드
|
||||
val coverImagePath = s3Uploader.upload(
|
||||
inputStream = coverImage.inputStream,
|
||||
bucket = coverImageBucket,
|
||||
filePath = "audio_content_cover/${audioContent.id}/$coverImageFileName",
|
||||
metadata = metadata
|
||||
)
|
||||
|
||||
audioContent.coverImage = coverImagePath
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteAudioContent(audioContentId: Long, member: Member) {
|
||||
val audioContent = repository.findByIdAndCreatorId(audioContentId, member.id!!)
|
||||
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
|
||||
|
||||
audioContent.isActive = false
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createAudioContent(
|
||||
contentFile: MultipartFile?,
|
||||
coverImage: MultipartFile?,
|
||||
requestString: String,
|
||||
member: Member
|
||||
): CreateAudioContentResponse {
|
||||
// coverImage 체크
|
||||
if (coverImage == null) throw SodaException("커버이미지를 선택해 주세요.")
|
||||
|
||||
// request 내용 파싱
|
||||
val request = objectMapper.readValue(requestString, CreateAudioContentRequest::class.java)
|
||||
|
||||
// contentFile 체크
|
||||
if (contentFile == null && request.type == AudioContentType.INDIVIDUAL) {
|
||||
throw SodaException("콘텐츠를 선택해 주세요.")
|
||||
}
|
||||
|
||||
if (request.type == AudioContentType.BUNDLE && request.childIds == null) {
|
||||
throw SodaException("묶음상품의 하위상품을 선택해 주세요.")
|
||||
}
|
||||
|
||||
// 테마 체크
|
||||
val theme = themeQueryRepository.findThemeByIdAndActive(id = request.themeId)
|
||||
?: throw SodaException("잘못된 테마입니다. 다시 선택해 주세요.")
|
||||
|
||||
if (request.price in 1..9) throw SodaException("콘텐츠의 최소금액은 10코인 입니다.")
|
||||
|
||||
// DB에 값 추가
|
||||
val audioContent = AudioContent(
|
||||
title = request.title,
|
||||
detail = request.detail,
|
||||
type = request.type,
|
||||
price = if (request.price < 0) {
|
||||
0
|
||||
} else {
|
||||
request.price
|
||||
},
|
||||
isAdult = request.isAdult,
|
||||
isGeneratePreview = if (request.type == AudioContentType.INDIVIDUAL) {
|
||||
request.isGeneratePreview
|
||||
} else {
|
||||
false
|
||||
},
|
||||
isCommentAvailable = request.isCommentAvailable
|
||||
)
|
||||
audioContent.theme = theme
|
||||
audioContent.member = member
|
||||
audioContent.isActive = request.type == AudioContentType.BUNDLE
|
||||
|
||||
repository.save(audioContent)
|
||||
|
||||
// 태그 분리, #추가, 등록
|
||||
if (request.tags.isNotBlank()) {
|
||||
val tags = request.tags
|
||||
.split(" ")
|
||||
.asSequence()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.map {
|
||||
val tag = if (!it.startsWith("#")) {
|
||||
"#$it"
|
||||
} else {
|
||||
it
|
||||
}
|
||||
|
||||
val hashTag = hashTagRepository.findByTag(tag)
|
||||
?: hashTagRepository.save(HashTag(tag))
|
||||
|
||||
val audioContentHashTag = AudioContentHashTag()
|
||||
audioContentHashTag.audioContent = audioContent
|
||||
audioContentHashTag.hashTag = hashTag
|
||||
|
||||
audioContentHashTag
|
||||
}.toList()
|
||||
|
||||
audioContent.audioContentHashTags.addAll(tags)
|
||||
}
|
||||
|
||||
var metadata = ObjectMetadata()
|
||||
metadata.contentLength = coverImage.size
|
||||
|
||||
// 커버 이미지 파일명 생성
|
||||
val coverImageFileName = generateFileName(prefix = "${audioContent.id}-cover")
|
||||
|
||||
// 커버 이미지 업로드
|
||||
val coverImagePath = s3Uploader.upload(
|
||||
inputStream = coverImage.inputStream,
|
||||
bucket = coverImageBucket,
|
||||
filePath = "audio_content_cover/${audioContent.id}/$coverImageFileName",
|
||||
metadata = metadata
|
||||
)
|
||||
|
||||
audioContent.coverImage = coverImagePath
|
||||
|
||||
if (contentFile != null && request.type == AudioContentType.INDIVIDUAL) {
|
||||
// 콘텐츠 파일명 생성
|
||||
val contentFileName = generateFileName(prefix = "${audioContent.id}-content")
|
||||
|
||||
// 콘텐츠 파일 업로드
|
||||
metadata = ObjectMetadata()
|
||||
metadata.contentLength = contentFile.size
|
||||
metadata.addUserMetadata("generate_preview", "true")
|
||||
|
||||
val contentPath = s3Uploader.upload(
|
||||
inputStream = contentFile.inputStream,
|
||||
bucket = audioContentBucket,
|
||||
filePath = "input/${audioContent.id}/$contentFileName",
|
||||
metadata = metadata
|
||||
)
|
||||
|
||||
audioContent.content = contentPath
|
||||
}
|
||||
|
||||
if (request.childIds != null && request.type == AudioContentType.BUNDLE) {
|
||||
for (childId in request.childIds) {
|
||||
val childContent = repository.findByIdAndActive(childId)
|
||||
?: continue
|
||||
|
||||
val bundleAudioContent = BundleAudioContent()
|
||||
bundleAudioContent.parent = audioContent
|
||||
bundleAudioContent.child = childContent
|
||||
audioContent.children.add(bundleAudioContent)
|
||||
}
|
||||
}
|
||||
|
||||
return CreateAudioContentResponse(contentId = audioContent.id!!)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun uploadComplete(contentId: Long, content: String, duration: String) {
|
||||
val keyFileName = content.split("/").last()
|
||||
if (!keyFileName.startsWith(contentId.toString())) throw SodaException("잘못된 요청입니다.")
|
||||
|
||||
val audioContent = repository.findByIdOrNull(contentId)
|
||||
?: throw SodaException("잘못된 요청입니다.")
|
||||
|
||||
audioContent.isActive = true
|
||||
audioContent.content = content
|
||||
audioContent.duration = duration
|
||||
}
|
||||
|
||||
fun getDetail(id: Long, member: Member, timezone: String): GetAudioContentDetailResponse {
|
||||
// 묶음 콘텐츠 조회
|
||||
val bundleAudioContentList = repository.findBundleByContentId(contentId = id)
|
||||
|
||||
// 오디오 콘텐츠 조회 (content_id, 제목, 내용, 테마, 태그, 19여부, 이미지, 콘텐츠 PATH)
|
||||
val audioContent = repository.findByIdOrNull(id)
|
||||
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
|
||||
|
||||
// 크리에이터(유저) 정보
|
||||
val creatorId = audioContent.member!!.id!!
|
||||
val creator = explorerQueryRepository.getAccount(creatorId)
|
||||
?: throw SodaException("없는 사용자 입니다.")
|
||||
|
||||
val notificationUserIds = explorerQueryRepository.getNotificationUserIds(creatorId)
|
||||
val isFollowing = notificationUserIds.contains(member.id)
|
||||
|
||||
// 차단된 사용자 체크
|
||||
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
|
||||
if (isBlocked) throw SodaException("${creator.nickname}님의 요청으로 콘텐츠 접근이 제한됩니다.")
|
||||
|
||||
// 구매 여부 확인
|
||||
val isExistsBundleAudioContent = bundleAudioContentList
|
||||
.map { orderRepository.isExistOrdered(memberId = member.id!!, contentId = it.id!!) }
|
||||
.contains(true)
|
||||
|
||||
val (isExistsAudioContent, orderType) = orderRepository.isExistOrderedAndOrderType(
|
||||
memberId = member.id!!,
|
||||
contentId = audioContent.id!!
|
||||
)
|
||||
|
||||
if (!isExistsAudioContent && !isExistsBundleAudioContent && !audioContent.isActive) {
|
||||
throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
|
||||
}
|
||||
|
||||
// 댓글
|
||||
val commentList = if (audioContent.isCommentAvailable) {
|
||||
commentRepository.findByContentId(
|
||||
cloudFrontHost = coverImageHost,
|
||||
contentId = audioContent.id!!,
|
||||
timezone = timezone,
|
||||
offset = 0,
|
||||
limit = 1
|
||||
)
|
||||
} else {
|
||||
listOf()
|
||||
}
|
||||
|
||||
// 댓글 수
|
||||
val commentCount = if (audioContent.isCommentAvailable) {
|
||||
commentRepository.totalCountCommentByContentId(contentId = audioContent.id!!)
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
val audioContentUrl = audioContentCloudFront.generateSignedURL(
|
||||
resourcePath = if (
|
||||
isExistsAudioContent ||
|
||||
isExistsBundleAudioContent ||
|
||||
audioContent.member!!.id!! == member.id!! ||
|
||||
audioContent.price <= 0
|
||||
) {
|
||||
audioContent.content!!
|
||||
} else {
|
||||
audioContent.content!!.replace("output/", "preview/")
|
||||
},
|
||||
expirationTime = 1000 * 60 * 60 * (audioContent.duration!!.split(":")[0].toLong() + 2)
|
||||
)
|
||||
|
||||
val tag = audioContent.audioContentHashTags
|
||||
.map { it.hashTag!!.tag }
|
||||
.joinToString(" ") { it }
|
||||
|
||||
val creatorOtherContentList = repository.getCreatorOtherContentList(
|
||||
cloudfrontHost = coverImageHost,
|
||||
contentId = audioContent.id!!,
|
||||
creatorId = creatorId,
|
||||
isAdult = member.auth != null
|
||||
)
|
||||
|
||||
val sameThemeOtherContentList = repository.getSameThemeOtherContentList(
|
||||
cloudfrontHost = coverImageHost,
|
||||
contentId = audioContent.id!!,
|
||||
themeId = audioContent.theme!!.id!!,
|
||||
isAdult = member.auth != null
|
||||
)
|
||||
|
||||
val likeCount = audioContentLikeRepository.totalCountAudioContentLike(contentId = id)
|
||||
val isLike = audioContentLikeRepository.findByMemberIdAndContentId(
|
||||
memberId = member.id!!,
|
||||
contentId = id
|
||||
)?.isActive ?: false
|
||||
|
||||
val remainingTime = if (orderType == OrderType.RENTAL) {
|
||||
orderRepository.getAudioContentRemainingTime(
|
||||
memberId = member.id!!,
|
||||
contentId = audioContent.id!!,
|
||||
timezone = timezone
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return GetAudioContentDetailResponse(
|
||||
contentId = audioContent.id!!,
|
||||
title = audioContent.title,
|
||||
detail = audioContent.detail,
|
||||
coverImageUrl = "$coverImageHost/${audioContent.coverImage!!}",
|
||||
contentUrl = audioContentUrl,
|
||||
themeStr = audioContent.theme!!.theme,
|
||||
tag = tag,
|
||||
price = audioContent.price,
|
||||
duration = audioContent.duration ?: "",
|
||||
isAdult = audioContent.isAdult,
|
||||
isMosaic = audioContent.isAdult && member.auth == null,
|
||||
existOrdered = isExistsBundleAudioContent || isExistsAudioContent,
|
||||
orderType = orderType,
|
||||
remainingTime = remainingTime,
|
||||
creatorOtherContentList = creatorOtherContentList,
|
||||
sameThemeOtherContentList = sameThemeOtherContentList,
|
||||
isCommentAvailable = audioContent.isCommentAvailable,
|
||||
isLike = isLike,
|
||||
likeCount = likeCount,
|
||||
commentList = commentList,
|
||||
commentCount = commentCount,
|
||||
creator = AudioContentCreator(
|
||||
creatorId = creatorId,
|
||||
nickname = creator.nickname,
|
||||
profileImageUrl = if (creator.profileImage != null) {
|
||||
"$coverImageHost/${creator.profileImage}"
|
||||
} else {
|
||||
"$coverImageHost/profile/default-profile.png"
|
||||
},
|
||||
isFollowing = isFollowing
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun getAudioContentList(
|
||||
creatorId: Long,
|
||||
sortType: SortType,
|
||||
member: Member,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): GetAudioContentListResponse {
|
||||
val totalCount = repository.findTotalCountByCreatorId(
|
||||
creatorId = creatorId,
|
||||
isAdult = member.auth != null
|
||||
)
|
||||
|
||||
val audioContentList = repository.findByCreatorId(
|
||||
creatorId = creatorId,
|
||||
isAdult = member.auth != null,
|
||||
sortType = sortType,
|
||||
offset = offset,
|
||||
limit = limit
|
||||
)
|
||||
|
||||
val items = audioContentList
|
||||
.map {
|
||||
val commentCount = commentRepository
|
||||
.totalCountCommentByContentId(it.id!!)
|
||||
|
||||
val likeCount = audioContentLikeRepository
|
||||
.totalCountAudioContentLike(it.id!!)
|
||||
|
||||
GetAudioContentListItem(
|
||||
contentId = it.id!!,
|
||||
coverImageUrl = "$coverImageHost/${it.coverImage!!}",
|
||||
title = it.title,
|
||||
price = it.price,
|
||||
themeStr = it.theme!!.theme,
|
||||
duration = it.duration,
|
||||
likeCount = likeCount,
|
||||
commentCount = commentCount,
|
||||
isAdult = it.isAdult
|
||||
)
|
||||
}
|
||||
|
||||
return GetAudioContentListResponse(
|
||||
totalCount = totalCount,
|
||||
items = items
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun addAllPlaybackTracking(request: AddAllPlaybackTrackingRequest, member: Member) {
|
||||
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
for (trackingData in request.trackingDataList) {
|
||||
val playDate = LocalDateTime.parse(trackingData.playDateTime, dateTimeFormatter)
|
||||
.atZone(ZoneId.of(request.timezone))
|
||||
.withZoneSameInstant(ZoneId.of("UTC"))
|
||||
.toLocalDateTime()
|
||||
|
||||
playbackTrackingRepository.save(
|
||||
PlaybackTracking(
|
||||
memberId = member.id!!,
|
||||
contentId = trackingData.contentId,
|
||||
playDate = playDate,
|
||||
isPreview = trackingData.isPreview
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.FetchType
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "bundle_content")
|
||||
data class BundleAudioContent(
|
||||
var isActive: Boolean = true
|
||||
) : BaseEntity() {
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "parent_content_id", nullable = false)
|
||||
var parent: AudioContent? = null
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "child_content_id", nullable = false)
|
||||
var child: AudioContent? = null
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
data class CreateAudioContentRequest(
|
||||
val title: String,
|
||||
val detail: String,
|
||||
val tags: String,
|
||||
val price: Int,
|
||||
val themeId: Long = 0,
|
||||
val isAdult: Boolean = false,
|
||||
val isGeneratePreview: Boolean = true,
|
||||
val isCommentAvailable: Boolean = false,
|
||||
val type: AudioContentType = AudioContentType.INDIVIDUAL,
|
||||
val childIds: List<Long>? = null
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
data class CreateAudioContentResponse(val contentId: Long)
|
|
@ -0,0 +1,43 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
import kr.co.vividnext.sodalive.content.comment.GetAudioContentCommentListItem
|
||||
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||
|
||||
data class GetAudioContentDetailResponse(
|
||||
val contentId: Long,
|
||||
val title: String,
|
||||
val detail: String,
|
||||
val coverImageUrl: String,
|
||||
val contentUrl: String,
|
||||
val themeStr: String,
|
||||
val tag: String,
|
||||
val price: Int,
|
||||
val duration: String,
|
||||
val isAdult: Boolean,
|
||||
val isMosaic: Boolean,
|
||||
val existOrdered: Boolean,
|
||||
val orderType: OrderType?,
|
||||
val remainingTime: String?,
|
||||
val creatorOtherContentList: List<OtherContentResponse>,
|
||||
val sameThemeOtherContentList: List<OtherContentResponse>,
|
||||
val isCommentAvailable: Boolean,
|
||||
val isLike: Boolean,
|
||||
val likeCount: Int,
|
||||
val commentList: List<GetAudioContentCommentListItem>,
|
||||
val commentCount: Int,
|
||||
val creator: AudioContentCreator
|
||||
)
|
||||
|
||||
data class OtherContentResponse @QueryProjection constructor(
|
||||
val contentId: Long,
|
||||
val title: String,
|
||||
val coverUrl: String
|
||||
)
|
||||
|
||||
data class AudioContentCreator(
|
||||
val creatorId: Long,
|
||||
val nickname: String,
|
||||
val profileImageUrl: String,
|
||||
val isFollowing: Boolean
|
||||
)
|
|
@ -0,0 +1,18 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
data class GetAudioContentListResponse(
|
||||
val totalCount: Int,
|
||||
val items: List<GetAudioContentListItem>
|
||||
)
|
||||
|
||||
data class GetAudioContentListItem(
|
||||
val contentId: Long,
|
||||
val coverImageUrl: String,
|
||||
val title: String,
|
||||
val price: Int,
|
||||
val themeStr: String,
|
||||
val duration: String?,
|
||||
val likeCount: Int,
|
||||
val commentCount: Int,
|
||||
val isAdult: Boolean
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
data class ModifyAudioContentRequest(
|
||||
val contentId: Long,
|
||||
val title: String?,
|
||||
val detail: String?,
|
||||
val isAdult: Boolean,
|
||||
val isCommentAvailable: Boolean
|
||||
)
|
|
@ -0,0 +1,26 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.GenerationType
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.PrePersist
|
||||
|
||||
@Entity
|
||||
data class PlaybackTracking(
|
||||
val memberId: Long,
|
||||
val contentId: Long,
|
||||
val isPreview: Boolean,
|
||||
val playDate: LocalDateTime
|
||||
) {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null
|
||||
var createdAt: LocalDateTime? = null
|
||||
|
||||
@PrePersist
|
||||
fun prePersist() {
|
||||
createdAt = LocalDateTime.now()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface PlaybackTrackingRepository : JpaRepository<PlaybackTracking, Long>
|
|
@ -0,0 +1,3 @@
|
|||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
data class UploadCompleteRequest(val contentId: Long, val contentPath: String, val duration: String)
|
|
@ -0,0 +1,41 @@
|
|||
package kr.co.vividnext.sodalive.content.comment
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.FetchType
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.OneToMany
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "content_comment")
|
||||
data class AudioContentComment(
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
var comment: String,
|
||||
@Column(nullable = true)
|
||||
var donationCan: Int? = null,
|
||||
var isActive: Boolean = true
|
||||
) : BaseEntity() {
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "parent_id", nullable = true)
|
||||
var parent: AudioContentComment? = null
|
||||
set(value) {
|
||||
value?.children?.add(this)
|
||||
field = value
|
||||
}
|
||||
|
||||
@OneToMany(mappedBy = "parent")
|
||||
var children: MutableList<AudioContentComment> = mutableListOf()
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", nullable = false)
|
||||
var member: Member? = null
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "content_id", nullable = false)
|
||||
var audioContent: AudioContent? = null
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package kr.co.vividnext.sodalive.content.comment
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
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.PutMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
class AudioContentCommentController(private val service: AudioContentCommentService) {
|
||||
@PostMapping("/audio-content/comment")
|
||||
fun registerComment(
|
||||
@RequestBody request: RegisterCommentRequest,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(
|
||||
service.registerComment(
|
||||
comment = request.comment,
|
||||
audioContentId = request.contentId,
|
||||
parentId = request.parentId,
|
||||
member = member
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@PutMapping("/audio-content/comment")
|
||||
fun modifyComment(
|
||||
@RequestBody request: ModifyCommentRequest,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(service.modifyComment(request = request, member = member))
|
||||
}
|
||||
|
||||
@GetMapping("/audio-content/{id}/comment")
|
||||
fun getCommentList(
|
||||
@PathVariable("id") audioContentId: Long,
|
||||
@RequestParam timezone: String,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
pageable: Pageable
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(
|
||||
service.getCommentList(
|
||||
audioContentId = audioContentId,
|
||||
timezone = timezone,
|
||||
pageable = pageable
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/audio-content/comment/{id}")
|
||||
fun getCommentReplyList(
|
||||
@PathVariable("id") commentId: Long,
|
||||
@RequestParam timezone: String,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
pageable: Pageable
|
||||
): ApiResponse<GetAudioContentCommentListResponse> {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
return ApiResponse.ok(
|
||||
service.getCommentReplyList(
|
||||
commentId = commentId,
|
||||
timezone = timezone,
|
||||
pageable = pageable
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package kr.co.vividnext.sodalive.content.comment
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.content.comment.QAudioContentComment.audioContentComment
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Repository
|
||||
interface AudioContentCommentRepository : JpaRepository<AudioContentComment, Long>, AudioContentCommentQueryRepository
|
||||
|
||||
interface AudioContentCommentQueryRepository {
|
||||
fun findByContentId(
|
||||
cloudFrontHost: String,
|
||||
contentId: Long,
|
||||
timezone: String,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<GetAudioContentCommentListItem>
|
||||
|
||||
fun totalCountCommentByContentId(contentId: Long): Int
|
||||
fun commentReplyCountByAudioContentCommentId(commentId: Long): Int
|
||||
fun getAudioContentCommentReplyList(
|
||||
cloudFrontHost: String,
|
||||
commentId: Long,
|
||||
timezone: String,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<GetAudioContentCommentListItem>
|
||||
}
|
||||
|
||||
@Repository
|
||||
class AudioContentCommentQueryRepositoryImpl(
|
||||
private val queryFactory: JPAQueryFactory
|
||||
) : AudioContentCommentQueryRepository {
|
||||
override fun findByContentId(
|
||||
cloudFrontHost: String,
|
||||
contentId: Long,
|
||||
timezone: String,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<GetAudioContentCommentListItem> {
|
||||
return queryFactory.selectFrom(audioContentComment)
|
||||
.where(
|
||||
audioContentComment.audioContent.id.eq(contentId)
|
||||
.and(audioContentComment.isActive.isTrue)
|
||||
.and(audioContentComment.parent.isNull)
|
||||
)
|
||||
.offset(offset)
|
||||
.limit(limit.toLong())
|
||||
.orderBy(audioContentComment.createdAt.desc())
|
||||
.fetch()
|
||||
.asSequence()
|
||||
.map {
|
||||
val date = it.createdAt!!
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.withZoneSameInstant(ZoneId.of(timezone))
|
||||
|
||||
GetAudioContentCommentListItem(
|
||||
id = it.id!!,
|
||||
writerId = it.member!!.id!!,
|
||||
nickname = it.member!!.nickname,
|
||||
profileUrl = if (it.member!!.profileImage != null) {
|
||||
"$cloudFrontHost/${it.member!!.profileImage}"
|
||||
} else {
|
||||
"$cloudFrontHost/profile/default-profile.png"
|
||||
},
|
||||
comment = it.comment,
|
||||
donationCoin = it.donationCan ?: 0,
|
||||
date = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")),
|
||||
replyCount = commentReplyCountByAudioContentCommentId(it.id!!)
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
override fun totalCountCommentByContentId(contentId: Long): Int {
|
||||
return queryFactory.select(audioContentComment.id)
|
||||
.from(audioContentComment)
|
||||
.where(
|
||||
audioContentComment.audioContent.id.eq(contentId)
|
||||
.and(audioContentComment.isActive.isTrue)
|
||||
)
|
||||
.fetch()
|
||||
.size
|
||||
}
|
||||
|
||||
override fun commentReplyCountByAudioContentCommentId(commentId: Long): Int {
|
||||
return queryFactory.select(audioContentComment.id)
|
||||
.from(audioContentComment)
|
||||
.where(
|
||||
audioContentComment.parent.isNotNull
|
||||
.and(audioContentComment.parent.id.eq(commentId))
|
||||
.and(audioContentComment.isActive.isTrue)
|
||||
)
|
||||
.fetch()
|
||||
.size
|
||||
}
|
||||
|
||||
override fun getAudioContentCommentReplyList(
|
||||
cloudFrontHost: String,
|
||||
commentId: Long,
|
||||
timezone: String,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<GetAudioContentCommentListItem> {
|
||||
return queryFactory.selectFrom(audioContentComment)
|
||||
.where(
|
||||
audioContentComment.parent.isNotNull
|
||||
.and(audioContentComment.parent.id.eq(commentId))
|
||||
.and(audioContentComment.isActive.isTrue)
|
||||
)
|
||||
.offset(offset)
|
||||
.limit(limit.toLong())
|
||||
.orderBy(audioContentComment.createdAt.desc())
|
||||
.fetch()
|
||||
.asSequence()
|
||||
.map {
|
||||
val date = it.createdAt!!
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.withZoneSameInstant(ZoneId.of(timezone))
|
||||
|
||||
GetAudioContentCommentListItem(
|
||||
id = it.id!!,
|
||||
writerId = it.member!!.id!!,
|
||||
nickname = it.member!!.nickname,
|
||||
profileUrl = if (it.member!!.profileImage != null) {
|
||||
"$cloudFrontHost/${it.member!!.profileImage}"
|
||||
} else {
|
||||
"$cloudFrontHost/profile/default-profile.png"
|
||||
},
|
||||
comment = it.comment,
|
||||
donationCoin = it.donationCan ?: 0,
|
||||
date = date.format(DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a")),
|
||||
replyCount = 0
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
package kr.co.vividnext.sodalive.content.comment
|
||||
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
class AudioContentCommentService(
|
||||
private val repository: AudioContentCommentRepository,
|
||||
private val audioContentRepository: AudioContentRepository,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val cloudFrontHost: String
|
||||
) {
|
||||
@Transactional
|
||||
fun registerComment(member: Member, comment: String, audioContentId: Long, parentId: Long? = null) {
|
||||
val audioContent = audioContentRepository.findByIdOrNull(id = audioContentId)
|
||||
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
|
||||
|
||||
val audioContentComment = AudioContentComment(comment = comment)
|
||||
audioContentComment.audioContent = audioContent
|
||||
audioContentComment.member = member
|
||||
|
||||
val parent = if (parentId != null) {
|
||||
repository.findByIdOrNull(id = parentId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (parent != null) {
|
||||
audioContentComment.parent = parent
|
||||
}
|
||||
|
||||
repository.save(audioContentComment)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun modifyComment(request: ModifyCommentRequest, member: Member) {
|
||||
val audioContentComment = repository.findByIdOrNull(request.commentId)
|
||||
?: throw SodaException("잘못된 접근 입니다.\n확인 후 다시 시도해 주세요.")
|
||||
|
||||
if (audioContentComment.audioContent!!.member!!.id!! != member.id!!) {
|
||||
if (audioContentComment.member == null || audioContentComment.member!!.id!! != member.id!!) {
|
||||
throw SodaException("잘못된 접근 입니다.\n확인 후 다시 시도해 주세요.")
|
||||
}
|
||||
|
||||
if (request.comment != null) {
|
||||
audioContentComment.comment = request.comment
|
||||
}
|
||||
}
|
||||
|
||||
if (request.isActive != null) {
|
||||
audioContentComment.isActive = request.isActive
|
||||
}
|
||||
}
|
||||
|
||||
fun getCommentList(audioContentId: Long, timezone: String, pageable: Pageable): GetAudioContentCommentListResponse {
|
||||
val commentList =
|
||||
repository.findByContentId(
|
||||
cloudFrontHost = cloudFrontHost,
|
||||
contentId = audioContentId,
|
||||
timezone = timezone,
|
||||
offset = pageable.offset,
|
||||
limit = pageable.pageSize
|
||||
)
|
||||
val totalCount = repository.totalCountCommentByContentId(audioContentId)
|
||||
|
||||
return GetAudioContentCommentListResponse(totalCount, commentList)
|
||||
}
|
||||
|
||||
fun getCommentReplyList(commentId: Long, timezone: String, pageable: Pageable): GetAudioContentCommentListResponse {
|
||||
val commentList = repository.getAudioContentCommentReplyList(
|
||||
cloudFrontHost = cloudFrontHost,
|
||||
commentId = commentId,
|
||||
timezone = timezone,
|
||||
offset = pageable.offset,
|
||||
limit = pageable.pageSize
|
||||
)
|
||||
val totalCount = repository.commentReplyCountByAudioContentCommentId(commentId)
|
||||
|
||||
return GetAudioContentCommentListResponse(totalCount, commentList)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package kr.co.vividnext.sodalive.content.comment
|
||||
|
||||
data class GetAudioContentCommentListResponse(
|
||||
val totalCount: Int,
|
||||
val items: List<GetAudioContentCommentListItem>
|
||||
)
|
||||
|
||||
data class GetAudioContentCommentListItem(
|
||||
val id: Long,
|
||||
val writerId: Long,
|
||||
val nickname: String,
|
||||
val profileUrl: String,
|
||||
val comment: String,
|
||||
val donationCoin: Int,
|
||||
val date: String,
|
||||
val replyCount: Int
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
package kr.co.vividnext.sodalive.content.comment
|
||||
|
||||
data class ModifyCommentRequest(val commentId: Long, val comment: String? = null, val isActive: Boolean? = null)
|
|
@ -0,0 +1,3 @@
|
|||
package kr.co.vividnext.sodalive.content.comment
|
||||
|
||||
data class RegisterCommentRequest(val comment: String, val contentId: Long, val parentId: Long?)
|
|
@ -0,0 +1,25 @@
|
|||
package kr.co.vividnext.sodalive.content.donation
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
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("/audio-content/donation")
|
||||
class AudioContentDonationController(private val service: AudioContentDonationService) {
|
||||
|
||||
@PostMapping
|
||||
fun donation(
|
||||
@RequestBody request: AudioContentDonationRequest,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(service.donation(request = request, member = member))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package kr.co.vividnext.sodalive.content.donation
|
||||
|
||||
data class AudioContentDonationRequest(
|
||||
val contentId: Long,
|
||||
val donationCan: Int,
|
||||
val comment: String,
|
||||
val container: String
|
||||
)
|
|
@ -0,0 +1,43 @@
|
|||
package kr.co.vividnext.sodalive.content.donation
|
||||
|
||||
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.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.content.comment.AudioContentComment
|
||||
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
class AudioContentDonationService(
|
||||
private val coinPaymentService: CanPaymentService,
|
||||
private val queryRepository: AudioContentRepository,
|
||||
private val commentRepository: AudioContentCommentRepository
|
||||
) {
|
||||
@Transactional
|
||||
fun donation(request: AudioContentDonationRequest, member: Member) {
|
||||
if (request.donationCan < 1) throw SodaException("1캔 이상 후원하실 수 있습니다.")
|
||||
if (request.comment.isBlank()) throw SodaException("함께 보낼 메시지를 입력하세요.")
|
||||
|
||||
val audioContent = queryRepository.findByIdAndActive(request.contentId)
|
||||
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
|
||||
|
||||
coinPaymentService.spendCan(
|
||||
memberId = member.id!!,
|
||||
needCan = request.donationCan,
|
||||
canUsage = CanUsage.DONATION,
|
||||
audioContent = audioContent,
|
||||
container = request.container
|
||||
)
|
||||
|
||||
val audioContentComment = AudioContentComment(
|
||||
comment = request.comment,
|
||||
donationCan = request.donationCan
|
||||
)
|
||||
audioContentComment.audioContent = audioContent
|
||||
audioContentComment.member = member
|
||||
commentRepository.save(audioContentComment)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package kr.co.vividnext.sodalive.content.hashtag
|
||||
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.FetchType
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.GenerationType
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.PrePersist
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "content_hash_tag")
|
||||
class AudioContentHashTag {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null
|
||||
|
||||
var createdAt: LocalDateTime? = null
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "content_id", nullable = false)
|
||||
var audioContent: AudioContent? = null
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "hash_tag_id", nullable = false)
|
||||
var hashTag: HashTag? = null
|
||||
|
||||
@PrePersist
|
||||
fun prePersist() {
|
||||
createdAt = LocalDateTime.now()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package kr.co.vividnext.sodalive.content.hashtag
|
||||
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.GenerationType
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.PrePersist
|
||||
|
||||
@Entity
|
||||
class HashTag(
|
||||
val tag: String
|
||||
) {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Int? = null
|
||||
var createdAt: LocalDateTime? = null
|
||||
|
||||
@PrePersist
|
||||
fun prePersist() {
|
||||
createdAt = LocalDateTime.now()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package kr.co.vividnext.sodalive.content.hashtag
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.content.hashtag.QHashTag.hashTag
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface HashTagRepository : JpaRepository<HashTag, Long>, HashTagQueryRepository
|
||||
|
||||
interface HashTagQueryRepository {
|
||||
fun findByTag(tag: String): HashTag?
|
||||
}
|
||||
|
||||
@Repository
|
||||
class HashTagQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : HashTagQueryRepository {
|
||||
override fun findByTag(tag: String): HashTag? {
|
||||
return queryFactory
|
||||
.selectFrom(hashTag)
|
||||
.where(hashTag.tag.eq(tag))
|
||||
.fetchOne()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package kr.co.vividnext.sodalive.content.like
|
||||
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.GeneratedValue
|
||||
import javax.persistence.GenerationType
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.PrePersist
|
||||
import javax.persistence.PreUpdate
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "content_like")
|
||||
data class AudioContentLike(
|
||||
val memberId: Long,
|
||||
val contentId: Long
|
||||
) {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null
|
||||
|
||||
var createdAt: LocalDateTime? = null
|
||||
var updatedAt: LocalDateTime? = null
|
||||
|
||||
@PrePersist
|
||||
fun prePersist() {
|
||||
createdAt = LocalDateTime.now()
|
||||
updatedAt = LocalDateTime.now()
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
fun preUpdate() {
|
||||
updatedAt = LocalDateTime.now()
|
||||
}
|
||||
|
||||
var isActive = true
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package kr.co.vividnext.sodalive.content.like
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.content.like.QAudioContentLike.audioContentLike
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface AudioContentLikeRepository : JpaRepository<AudioContentLike, Long>, AudioContentLikeQueryRepository
|
||||
|
||||
interface AudioContentLikeQueryRepository {
|
||||
fun findByMemberIdAndContentId(memberId: Long, contentId: Long): AudioContentLike?
|
||||
fun totalCountAudioContentLike(contentId: Long): Int
|
||||
}
|
||||
|
||||
@Repository
|
||||
class AudioContentLikeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AudioContentLikeQueryRepository {
|
||||
override fun findByMemberIdAndContentId(memberId: Long, contentId: Long): AudioContentLike? {
|
||||
return queryFactory
|
||||
.selectFrom(audioContentLike)
|
||||
.where(
|
||||
audioContentLike.memberId.eq(memberId)
|
||||
.and(audioContentLike.contentId.eq(contentId))
|
||||
)
|
||||
.fetchFirst()
|
||||
}
|
||||
|
||||
override fun totalCountAudioContentLike(contentId: Long): Int {
|
||||
return queryFactory
|
||||
.select(audioContentLike.id)
|
||||
.from(audioContentLike)
|
||||
.where(
|
||||
audioContentLike.contentId.eq(contentId)
|
||||
.and(audioContentLike.isActive.isTrue)
|
||||
)
|
||||
.fetch()
|
||||
.size
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package kr.co.vividnext.sodalive.content.like
|
||||
|
||||
data class PutAudioContentLikeRequest(val contentId: Long)
|
|
@ -0,0 +1,3 @@
|
|||
package kr.co.vividnext.sodalive.content.like
|
||||
|
||||
data class PutAudioContentLikeResponse(val like: Boolean)
|
|
@ -0,0 +1,34 @@
|
|||
package kr.co.vividnext.sodalive.content.main
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/audio-content/main")
|
||||
class AudioContentMainController(private val service: AudioContentMainService) {
|
||||
|
||||
@GetMapping
|
||||
fun getMain(
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(service.getMain(member = member))
|
||||
}
|
||||
|
||||
@GetMapping("/new")
|
||||
fun getNewContentByTheme(
|
||||
@RequestParam("theme") theme: String,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(service.getNewContentByTheme(theme, member))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package kr.co.vividnext.sodalive.content.main
|
||||
|
||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
|
||||
import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse
|
||||
import kr.co.vividnext.sodalive.content.main.curation.GetAudioContentCurationResponse
|
||||
import kr.co.vividnext.sodalive.content.order.OrderService
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
|
||||
import kr.co.vividnext.sodalive.event.EventItem
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class AudioContentMainService(
|
||||
private val repository: AudioContentRepository,
|
||||
private val blockMemberRepository: BlockMemberRepository,
|
||||
private val orderService: OrderService,
|
||||
private val audioContentThemeRepository: AudioContentThemeQueryRepository,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val imageHost: String
|
||||
) {
|
||||
fun getMain(member: Member): GetAudioContentMainResponse {
|
||||
val isAdult = member.auth != null
|
||||
|
||||
// 2주일 이내에 콘텐츠를 올린 크리에이터 20명 조회
|
||||
val newContentUploadCreatorList = repository.getNewContentUploadCreatorList(
|
||||
cloudfrontHost = imageHost,
|
||||
isAdult = isAdult
|
||||
)
|
||||
.asSequence()
|
||||
.filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creatorId) }
|
||||
.toList()
|
||||
|
||||
val bannerList = repository.getAudioContentMainBannerList(isAdult = isAdult)
|
||||
.asSequence()
|
||||
.filter {
|
||||
if (it.type == AudioContentBannerType.CREATOR && it.creator != null) {
|
||||
!blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creator!!.id!!)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
.map {
|
||||
GetAudioContentBannerResponse(
|
||||
type = it.type,
|
||||
thumbnailImageUrl = "$imageHost/${it.thumbnailImage}",
|
||||
eventItem = if (it.type == AudioContentBannerType.EVENT && it.event != null) {
|
||||
EventItem(
|
||||
id = it.event!!.id!!,
|
||||
thumbnailImageUrl = if (!it.event!!.thumbnailImage.startsWith("https://")) {
|
||||
"$imageHost/${it.event!!.thumbnailImage}"
|
||||
} else {
|
||||
it.event!!.thumbnailImage
|
||||
},
|
||||
detailImageUrl = if (
|
||||
it.event!!.detailImage != null &&
|
||||
!it.event!!.detailImage!!.startsWith("https://")
|
||||
) {
|
||||
"$imageHost/${it.event!!.detailImage}"
|
||||
} else {
|
||||
it.event!!.detailImage
|
||||
},
|
||||
popupImageUrl = null,
|
||||
link = it.event!!.link,
|
||||
title = it.event!!.title,
|
||||
isPopup = false
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
creatorId = if (it.type == AudioContentBannerType.CREATOR && it.creator != null) {
|
||||
it.creator!!.id
|
||||
} else {
|
||||
null
|
||||
},
|
||||
link = it.link
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
|
||||
// 구매목록 20개
|
||||
val orderList = orderService.getAudioContentMainOrderList(
|
||||
member = member,
|
||||
limit = 20
|
||||
)
|
||||
|
||||
// 콘텐츠 테마
|
||||
val themeList = audioContentThemeRepository.getActiveThemeOfContent(isAdult = isAdult)
|
||||
|
||||
// 새 콘텐츠 20개 - 시간 내림차순 정렬
|
||||
val newContentList = repository.findByTheme(
|
||||
cloudfrontHost = imageHost,
|
||||
isAdult = isAdult
|
||||
)
|
||||
.asSequence()
|
||||
.filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creatorId) }
|
||||
.toList()
|
||||
|
||||
val curationList = repository
|
||||
.getAudioContentCurations(isAdult = isAdult)
|
||||
.asSequence()
|
||||
.map {
|
||||
GetAudioContentCurationResponse(
|
||||
title = it.title,
|
||||
description = it.description,
|
||||
contents = repository.findAudioContentByCurationId(
|
||||
curationId = it.id!!,
|
||||
cloudfrontHost = imageHost,
|
||||
isAdult = isAdult
|
||||
)
|
||||
.asSequence()
|
||||
.filter { content ->
|
||||
!blockMemberRepository.isBlocked(
|
||||
blockedMemberId = member.id!!,
|
||||
memberId = content.creatorId
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
)
|
||||
}
|
||||
.filter { it.contents.isNotEmpty() }
|
||||
.toList()
|
||||
|
||||
return GetAudioContentMainResponse(
|
||||
newContentUploadCreatorList = newContentUploadCreatorList,
|
||||
bannerList = bannerList,
|
||||
orderList = orderList,
|
||||
themeList = themeList,
|
||||
newContentList = newContentList,
|
||||
curationList = curationList
|
||||
)
|
||||
}
|
||||
|
||||
fun getNewContentByTheme(theme: String, member: Member): List<GetAudioContentMainItem> {
|
||||
return repository.findByTheme(
|
||||
cloudfrontHost = imageHost,
|
||||
theme = theme,
|
||||
isAdult = member.auth != null
|
||||
)
|
||||
.asSequence()
|
||||
.filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creatorId) }
|
||||
.toList()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package kr.co.vividnext.sodalive.content.main
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
|
||||
data class GetAudioContentMainItem @QueryProjection constructor(
|
||||
val contentId: Long,
|
||||
val coverImageUrl: String,
|
||||
val title: String,
|
||||
val isAdult: Boolean,
|
||||
val creatorId: Long,
|
||||
val creatorProfileImageUrl: String,
|
||||
val creatorNickname: String
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
package kr.co.vividnext.sodalive.content.main
|
||||
|
||||
import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse
|
||||
import kr.co.vividnext.sodalive.content.main.curation.GetAudioContentCurationResponse
|
||||
|
||||
data class GetAudioContentMainResponse(
|
||||
val newContentUploadCreatorList: List<GetNewContentUploadCreator>,
|
||||
val bannerList: List<GetAudioContentBannerResponse>,
|
||||
val orderList: List<GetAudioContentMainItem>,
|
||||
val themeList: List<String>,
|
||||
val newContentList: List<GetAudioContentMainItem>,
|
||||
val curationList: List<GetAudioContentCurationResponse>
|
||||
)
|
|
@ -0,0 +1,9 @@
|
|||
package kr.co.vividnext.sodalive.content.main
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
|
||||
data class GetNewContentUploadCreator @QueryProjection constructor(
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileImageUrl: String
|
||||
)
|
|
@ -0,0 +1,43 @@
|
|||
package kr.co.vividnext.sodalive.content.main.banner
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import kr.co.vividnext.sodalive.event.Event
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
import javax.persistence.Enumerated
|
||||
import javax.persistence.FetchType
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.OneToOne
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "content_banner")
|
||||
data class AudioContentBanner(
|
||||
@Column(nullable = false)
|
||||
var thumbnailImage: String = "",
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
var type: AudioContentBannerType,
|
||||
@Column(nullable = false)
|
||||
var isAdult: Boolean = false,
|
||||
@Column(nullable = false)
|
||||
var isActive: Boolean = true,
|
||||
@Column(nullable = false)
|
||||
var orders: Int = 1
|
||||
) : BaseEntity() {
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "event_id", nullable = true)
|
||||
var event: Event? = null
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "creator_id", nullable = true)
|
||||
var creator: Member? = null
|
||||
|
||||
@Column(nullable = true)
|
||||
var link: String? = null
|
||||
}
|
||||
|
||||
enum class AudioContentBannerType {
|
||||
EVENT, CREATOR, LINK
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package kr.co.vividnext.sodalive.content.main.banner
|
||||
|
||||
import kr.co.vividnext.sodalive.event.EventItem
|
||||
|
||||
data class GetAudioContentBannerResponse(
|
||||
val type: AudioContentBannerType,
|
||||
val thumbnailImageUrl: String,
|
||||
val eventItem: EventItem?,
|
||||
val creatorId: Long?,
|
||||
val link: String?
|
||||
)
|
|
@ -0,0 +1,26 @@
|
|||
package kr.co.vividnext.sodalive.content.main.curation
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.OneToMany
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "content_curation")
|
||||
data class AudioContentCuration(
|
||||
@Column(nullable = false)
|
||||
var title: String,
|
||||
@Column(nullable = false)
|
||||
var description: String,
|
||||
@Column(nullable = false)
|
||||
var isAdult: Boolean = false,
|
||||
@Column(nullable = false)
|
||||
var isActive: Boolean = true,
|
||||
@Column(nullable = false)
|
||||
var orders: Int = 1
|
||||
) : BaseEntity() {
|
||||
@OneToMany(mappedBy = "curation")
|
||||
val audioContents: MutableList<AudioContent> = mutableListOf()
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package kr.co.vividnext.sodalive.content.main.curation
|
||||
|
||||
import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem
|
||||
|
||||
data class GetAudioContentCurationResponse(
|
||||
val title: String,
|
||||
val description: String,
|
||||
val contents: List<GetAudioContentMainItem>
|
||||
)
|
|
@ -0,0 +1,20 @@
|
|||
package kr.co.vividnext.sodalive.content.order
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
|
||||
data class GetAudioContentOrderListResponse(
|
||||
val totalCount: Int,
|
||||
val items: List<GetAudioContentOrderListItem>
|
||||
)
|
||||
|
||||
data class GetAudioContentOrderListItem @QueryProjection constructor(
|
||||
val contentId: Long,
|
||||
val coverImageUrl: String,
|
||||
val title: String,
|
||||
val themeStr: String,
|
||||
val duration: String?,
|
||||
val isAdult: Boolean,
|
||||
val orderType: OrderType,
|
||||
var likeCount: Int = 0,
|
||||
var commentCount: Int = 0
|
||||
)
|
|
@ -0,0 +1,58 @@
|
|||
package kr.co.vividnext.sodalive.content.order
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
import javax.persistence.Enumerated
|
||||
import javax.persistence.FetchType
|
||||
import javax.persistence.JoinColumn
|
||||
import javax.persistence.ManyToOne
|
||||
import javax.persistence.Table
|
||||
import kotlin.math.ceil
|
||||
|
||||
enum class OrderType {
|
||||
RENTAL, KEEP
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "orders")
|
||||
data class Order(
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
val type: OrderType,
|
||||
var isActive: Boolean = true
|
||||
) : BaseEntity() {
|
||||
var can: Int = 0
|
||||
|
||||
val startDate: LocalDateTime = LocalDateTime.now()
|
||||
var endDate: LocalDateTime? = null
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "creator_id", nullable = false)
|
||||
var creator: Member? = null
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", nullable = false)
|
||||
var member: Member? = null
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "content_id", nullable = false)
|
||||
var audioContent: AudioContent? = null
|
||||
set(value) {
|
||||
can = if (type == OrderType.RENTAL) {
|
||||
ceil(value?.price!! * 0.7).toInt()
|
||||
} else {
|
||||
value?.price!!
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
override fun prePersist() {
|
||||
super.prePersist()
|
||||
if (type == OrderType.RENTAL) {
|
||||
endDate = startDate.plusDays(7)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
package kr.co.vividnext.sodalive.content.order
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
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("/order")
|
||||
class OrderController(private val service: OrderService) {
|
||||
@PostMapping("/audio-content")
|
||||
fun order(
|
||||
@RequestBody request: OrderRequest,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(
|
||||
service.order(
|
||||
request.contentId,
|
||||
request.orderType,
|
||||
request.container,
|
||||
member
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@GetMapping("/audio-content")
|
||||
fun getAudioContentOrderList(
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
pageable: Pageable
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(
|
||||
service.getAudioContentOrderList(
|
||||
member = member,
|
||||
offset = pageable.offset,
|
||||
limit = pageable.pageSize.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
package kr.co.vividnext.sodalive.content.order
|
||||
|
||||
import com.querydsl.core.types.dsl.Expressions
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||
import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem
|
||||
import kr.co.vividnext.sodalive.content.main.QGetAudioContentMainItem
|
||||
import kr.co.vividnext.sodalive.content.order.QOrder.order
|
||||
import kr.co.vividnext.sodalive.member.QMember.member
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.Duration
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
interface OrderRepository : JpaRepository<Order, Long>, OrderQueryRepository
|
||||
|
||||
interface OrderQueryRepository {
|
||||
fun isExistOrdered(memberId: Long, contentId: Long): Boolean
|
||||
fun isExistOrderedAndOrderType(memberId: Long, contentId: Long): Pair<Boolean, OrderType?>
|
||||
fun getAudioContentRemainingTime(memberId: Long, contentId: Long, timezone: String): String
|
||||
fun getAudioContentOrderList(
|
||||
dateTime: LocalDateTime,
|
||||
coverImageHost: String,
|
||||
memberId: Long,
|
||||
offset: Long = 0,
|
||||
limit: Long = 10
|
||||
): List<GetAudioContentOrderListItem>
|
||||
|
||||
fun totalAudioContentOrderListCount(memberId: Long, dateTime: LocalDateTime): Int
|
||||
fun getAudioContentMainOrderList(
|
||||
dateTime: LocalDateTime,
|
||||
coverImageHost: String,
|
||||
memberId: Long,
|
||||
offset: Long = 0,
|
||||
limit: Long = 20
|
||||
): List<GetAudioContentMainItem>
|
||||
}
|
||||
|
||||
@Repository
|
||||
class OrderQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : OrderQueryRepository {
|
||||
override fun isExistOrdered(memberId: Long, contentId: Long): Boolean {
|
||||
return queryFactory
|
||||
.select(order.id)
|
||||
.from(order)
|
||||
.where(
|
||||
order.member.id.eq(memberId)
|
||||
.and(order.audioContent.id.eq(contentId))
|
||||
.and(order.isActive.isTrue)
|
||||
.and(
|
||||
order.type.eq(OrderType.KEEP)
|
||||
.or(
|
||||
order.type.eq(OrderType.RENTAL)
|
||||
.and(order.endDate.after(LocalDateTime.now()))
|
||||
)
|
||||
)
|
||||
)
|
||||
.fetch()
|
||||
.isNotEmpty()
|
||||
}
|
||||
|
||||
override fun isExistOrderedAndOrderType(memberId: Long, contentId: Long): Pair<Boolean, OrderType?> {
|
||||
val result = queryFactory
|
||||
.select(order.type)
|
||||
.from(order)
|
||||
.where(
|
||||
order.member.id.eq(memberId)
|
||||
.and(order.audioContent.id.eq(contentId))
|
||||
.and(order.isActive.isTrue)
|
||||
.and(
|
||||
order.type.eq(OrderType.KEEP)
|
||||
.or(
|
||||
order.type.eq(OrderType.RENTAL)
|
||||
.and(order.endDate.after(LocalDateTime.now()))
|
||||
)
|
||||
)
|
||||
)
|
||||
.fetch()
|
||||
|
||||
val isExist = result.isNotEmpty()
|
||||
|
||||
return if (!isExist) {
|
||||
Pair(false, null)
|
||||
} else {
|
||||
if (result.contains(OrderType.KEEP)) {
|
||||
Pair(true, OrderType.KEEP)
|
||||
} else {
|
||||
Pair(true, OrderType.RENTAL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAudioContentRemainingTime(memberId: Long, contentId: Long, timezone: String): String {
|
||||
val result = queryFactory
|
||||
.select(order.endDate)
|
||||
.from(order)
|
||||
.where(
|
||||
order.member.id.eq(memberId)
|
||||
.and(order.audioContent.id.eq(contentId))
|
||||
.and(order.isActive.isTrue)
|
||||
.and(
|
||||
order.type.eq(OrderType.KEEP)
|
||||
.or(
|
||||
order.type.eq(OrderType.RENTAL)
|
||||
.and(order.endDate.after(LocalDateTime.now()))
|
||||
)
|
||||
)
|
||||
)
|
||||
.fetchFirst() ?: return ""
|
||||
|
||||
val duration = Duration.between(LocalDateTime.now(), result)
|
||||
|
||||
return duration.toHours().toString().padStart(2, '0') + "시간 " +
|
||||
duration.toMinutesPart().toString().padStart(2, '0') + "분"
|
||||
}
|
||||
|
||||
override fun getAudioContentOrderList(
|
||||
dateTime: LocalDateTime,
|
||||
coverImageHost: String,
|
||||
memberId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<GetAudioContentOrderListItem> {
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetAudioContentOrderListItem(
|
||||
audioContent.id,
|
||||
audioContent.coverImage.prepend("/").prepend(coverImageHost),
|
||||
audioContent.title,
|
||||
audioContent.theme.theme,
|
||||
audioContent.duration,
|
||||
audioContent.isAdult,
|
||||
order.type,
|
||||
Expressions.ZERO,
|
||||
Expressions.ZERO
|
||||
)
|
||||
)
|
||||
.from(order)
|
||||
.innerJoin(order.audioContent, audioContent)
|
||||
.where(
|
||||
order.member.id.eq(memberId)
|
||||
.and(
|
||||
order.type.eq(OrderType.KEEP)
|
||||
.or(
|
||||
order.type.eq(OrderType.RENTAL)
|
||||
.and(order.startDate.before(dateTime))
|
||||
.and(order.endDate.after(dateTime))
|
||||
)
|
||||
)
|
||||
)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.orderBy(order.createdAt.desc())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun totalAudioContentOrderListCount(memberId: Long, dateTime: LocalDateTime): Int {
|
||||
return queryFactory.select(order.id)
|
||||
.from(order)
|
||||
.where(
|
||||
order.member.id.eq(memberId)
|
||||
.and(
|
||||
order.type.eq(OrderType.KEEP)
|
||||
.or(
|
||||
order.type.eq(OrderType.RENTAL)
|
||||
.and(order.startDate.before(dateTime))
|
||||
.and(order.endDate.after(dateTime))
|
||||
)
|
||||
)
|
||||
)
|
||||
.fetch()
|
||||
.size
|
||||
}
|
||||
|
||||
override fun getAudioContentMainOrderList(
|
||||
dateTime: LocalDateTime,
|
||||
coverImageHost: String,
|
||||
memberId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<GetAudioContentMainItem> {
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetAudioContentMainItem(
|
||||
audioContent.id,
|
||||
audioContent.coverImage.prepend("/").prepend(coverImageHost),
|
||||
audioContent.title,
|
||||
audioContent.isAdult,
|
||||
member.id,
|
||||
member.profileImage.nullif("profile/default-profile.png")
|
||||
.prepend("/")
|
||||
.prepend(coverImageHost),
|
||||
member.nickname
|
||||
)
|
||||
)
|
||||
.from(order)
|
||||
.innerJoin(order.audioContent, audioContent)
|
||||
.innerJoin(audioContent.member, member)
|
||||
.where(
|
||||
order.member.id.eq(memberId)
|
||||
.and(
|
||||
order.type.eq(OrderType.KEEP)
|
||||
.or(
|
||||
order.type.eq(OrderType.RENTAL)
|
||||
.and(order.startDate.before(dateTime))
|
||||
.and(order.endDate.after(dateTime))
|
||||
)
|
||||
)
|
||||
)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.orderBy(order.createdAt.desc())
|
||||
.fetch()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package kr.co.vividnext.sodalive.content.order
|
||||
|
||||
data class OrderRequest(val contentId: Long, val orderType: OrderType, val container: String)
|
|
@ -0,0 +1,99 @@
|
|||
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.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
|
||||
import kr.co.vividnext.sodalive.content.like.AudioContentLikeRepository
|
||||
import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
class OrderService(
|
||||
private val repository: OrderRepository,
|
||||
private val coinPaymentService: CanPaymentService,
|
||||
private val audioContentRepository: AudioContentRepository,
|
||||
private val audioContentCommentQueryRepository: AudioContentCommentRepository,
|
||||
private val audioContentLikeQueryRepository: AudioContentLikeRepository,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val audioContentCoverImageHost: String
|
||||
) {
|
||||
@Transactional
|
||||
fun order(contentId: Long, orderType: OrderType, container: String, member: Member) {
|
||||
val order = Order(type = orderType)
|
||||
val content = audioContentRepository.findByIdAndActive(contentId)
|
||||
?: throw SodaException("잘못된 콘텐츠 입니다\n다시 시도해 주세요.")
|
||||
|
||||
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)
|
||||
|
||||
coinPaymentService.spendCan(
|
||||
memberId = member.id!!,
|
||||
needCan = order.can,
|
||||
canUsage = CanUsage.ORDER_CONTENT,
|
||||
order = order,
|
||||
container = container
|
||||
)
|
||||
}
|
||||
|
||||
fun getAudioContentOrderList(
|
||||
member: Member,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): GetAudioContentOrderListResponse {
|
||||
val totalCount = repository.totalAudioContentOrderListCount(
|
||||
memberId = member.id!!,
|
||||
dateTime = LocalDateTime.now()
|
||||
)
|
||||
val orderItems = repository.getAudioContentOrderList(
|
||||
dateTime = LocalDateTime.now(),
|
||||
coverImageHost = audioContentCoverImageHost,
|
||||
memberId = member.id!!,
|
||||
offset = offset,
|
||||
limit = limit
|
||||
)
|
||||
.asSequence()
|
||||
.map {
|
||||
val commentCount = audioContentCommentQueryRepository
|
||||
.totalCountCommentByContentId(it.contentId)
|
||||
|
||||
val likeCount = audioContentLikeQueryRepository
|
||||
.totalCountAudioContentLike(it.contentId)
|
||||
|
||||
it.commentCount = commentCount
|
||||
it.likeCount = likeCount
|
||||
it
|
||||
}
|
||||
.toList()
|
||||
|
||||
return GetAudioContentOrderListResponse(
|
||||
totalCount = totalCount,
|
||||
items = orderItems
|
||||
)
|
||||
}
|
||||
|
||||
fun getAudioContentMainOrderList(member: Member, limit: Int): List<GetAudioContentMainItem> {
|
||||
return repository.getAudioContentMainOrderList(
|
||||
dateTime = LocalDateTime.now(),
|
||||
coverImageHost = audioContentCoverImageHost,
|
||||
memberId = member.id!!,
|
||||
offset = 0,
|
||||
limit = 20
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package kr.co.vividnext.sodalive.content.theme
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Table
|
||||
|
||||
@Entity
|
||||
@Table(name = "content_theme")
|
||||
data class AudioContentTheme(
|
||||
@Column(nullable = false)
|
||||
var theme: String,
|
||||
@Column(nullable = false)
|
||||
var image: String,
|
||||
@Column(nullable = false)
|
||||
var isActive: Boolean = true,
|
||||
@Column(nullable = false)
|
||||
var orders: Int = 1
|
||||
) : BaseEntity()
|
|
@ -0,0 +1,24 @@
|
|||
package kr.co.vividnext.sodalive.content.theme
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/audio-content/theme")
|
||||
@PreAuthorize("hasRole('CREATOR')")
|
||||
class AudioContentThemeController(private val service: AudioContentThemeService) {
|
||||
@GetMapping
|
||||
fun getThemes(
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
ApiResponse.ok(service.getThemes())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package kr.co.vividnext.sodalive.content.theme
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
|
||||
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
class AudioContentThemeQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory,
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val cloudFrontHost: String
|
||||
) {
|
||||
fun getActiveThemes(): List<GetAudioContentThemeResponse> {
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetAudioContentThemeResponse(
|
||||
audioContentTheme.id,
|
||||
audioContentTheme.theme,
|
||||
audioContentTheme.image.prepend("/").prepend(cloudFrontHost)
|
||||
)
|
||||
)
|
||||
.from(audioContentTheme)
|
||||
.where(audioContentTheme.isActive.isTrue)
|
||||
.orderBy(audioContentTheme.orders.asc())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
fun getActiveThemeOfContent(isAdult: Boolean = false): List<String> {
|
||||
var where = audioContent.isActive.isTrue
|
||||
.and(audioContentTheme.isActive.isTrue)
|
||||
|
||||
if (!isAdult) {
|
||||
where = where.and(audioContent.isAdult.isFalse)
|
||||
}
|
||||
|
||||
return queryFactory
|
||||
.select(audioContentTheme.theme)
|
||||
.from(audioContent)
|
||||
.innerJoin(audioContent.theme, audioContentTheme)
|
||||
.where(where)
|
||||
.groupBy(audioContentTheme.id)
|
||||
.orderBy(audioContentTheme.orders.asc())
|
||||
.fetch()
|
||||
}
|
||||
|
||||
fun findThemeByIdAndActive(id: Long): AudioContentTheme? {
|
||||
return queryFactory
|
||||
.selectFrom(audioContentTheme)
|
||||
.where(
|
||||
audioContentTheme.id.eq(id)
|
||||
.and(audioContentTheme.isActive.isTrue)
|
||||
)
|
||||
.fetchFirst()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package kr.co.vividnext.sodalive.content.theme
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class AudioContentThemeService(private val queryRepository: AudioContentThemeQueryRepository) {
|
||||
fun getThemes(): List<GetAudioContentThemeResponse> {
|
||||
return queryRepository.getActiveThemes()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package kr.co.vividnext.sodalive.content.theme
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
|
||||
data class GetAudioContentThemeResponse @QueryProjection constructor(
|
||||
val id: Long,
|
||||
val theme: String,
|
||||
val image: String
|
||||
)
|
|
@ -26,7 +26,12 @@ cloud:
|
|||
accessKey: ${APP_AWS_ACCESS_KEY}
|
||||
secretKey: ${APP_AWS_SECRET_KEY}
|
||||
s3:
|
||||
contentBucket: ${S3_CONTENT_BUCKET}
|
||||
bucket: ${S3_BUCKET}
|
||||
contentCloudFront:
|
||||
host: ${CONTENT_CLOUD_FRONT_HOST}
|
||||
privateKeyFilePath: ${CONTENT_CLOUD_FRONT_PRIVATE_KEY_FILE_PATH}
|
||||
keyPairId: ${CONTENT_CLOUD_FRONT_KEY_PAIR_ID}
|
||||
cloudFront:
|
||||
host: ${CLOUD_FRONT_HOST}
|
||||
region:
|
||||
|
|
Loading…
Reference in New Issue