sodalive-backend-spring-boot/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt

831 lines
31 KiB
Kotlin

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.common.annotation.AudioContentReleaseSchedulerOnly
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.main.GetAudioContentRanking
import kr.co.vividnext.sodalive.content.order.LimitedEditionOrderRepository
import kr.co.vividnext.sodalive.content.order.OrderRepository
import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.pin.PinContent
import kr.co.vividnext.sodalive.content.pin.PinContentRepository
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType
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.cache.annotation.Cacheable
import org.springframework.context.ApplicationEventPublisher
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.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Locale
@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 limitedEditionOrderRepository: LimitedEditionOrderRepository,
private val themeQueryRepository: AudioContentThemeQueryRepository,
private val playbackTrackingRepository: PlaybackTrackingRepository,
private val commentRepository: AudioContentCommentRepository,
private val audioContentLikeRepository: AudioContentLikeRepository,
private val pinContentRepository: PinContentRepository,
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
private val audioContentCloudFront: AudioContentCloudFront,
private val applicationEventPublisher: ApplicationEventPublisher,
@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!!)
val audioContent = repository.findByIdAndActive(request.contentId)
audioContentLike.audioContent = audioContent
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
audioContent.releaseDate = null
}
@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)
// 미리듣기 시간 체크
validatePreviewTime(request.previewStartTime, request.previewEndTime)
val releaseDate = if (request.releaseDate != null) {
request.releaseDate.convertLocalDateTime("yyyy-MM-dd HH:mm")
.atZone(ZoneId.of(request.timezone))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
} else {
LocalDateTime.now()
}
// contentFile 체크
if (contentFile == null) {
throw SodaException("콘텐츠를 선택해 주세요.")
}
// 테마 체크
val theme = themeQueryRepository.findThemeByIdAndActive(id = request.themeId)
?: throw SodaException("잘못된 테마입니다. 다시 선택해 주세요.")
if ((request.themeId == 12L || request.themeId == 13L || request.themeId == 14L) && request.price < 5) {
throw SodaException("알람, 모닝콜, 슬립콜 테마의 콘텐츠는 5캔 이상의 유료콘텐츠로 등록이 가능합니다.")
}
if (request.price in 1..4) throw SodaException("콘텐츠의 최소금액은 5캔 입니다.")
val isOnlyRental = if (request.limited != null && request.limited > 0) {
false
} else if (request.purchaseOption == PurchaseOption.RENT_ONLY) {
true
} else {
request.isOnlyRental
}
val isFullDetailVisible = if (request.price >= 50) {
request.isFullDetailVisible
} else {
true
}
val purchaseOption = if (request.themeId == 12L || request.themeId == 13L || request.themeId == 14L) {
PurchaseOption.BUY_ONLY
} else {
request.purchaseOption
}
// DB에 값 추가
val audioContent = AudioContent(
title = request.title,
detail = request.detail,
price = if (request.price > 0) {
request.price
} else {
0
},
releaseDate = releaseDate,
limited = request.limited,
remaining = request.limited,
isAdult = request.isAdult,
purchaseOption = purchaseOption,
isGeneratePreview = request.isGeneratePreview,
isOnlyRental = isOnlyRental,
isCommentAvailable = request.isCommentAvailable,
isFullDetailVisible = isFullDetailVisible
)
audioContent.theme = theme
audioContent.member = member
repository.save(audioContent)
// 태그 분리, #추가, 등록
if (request.tags.isNotBlank()) {
val tags = request.tags
.replace("#", " #")
.split(" ")
.asSequence()
.map { it.trim() }
.filter { it.isNotBlank() }
.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
// 콘텐츠 파일명 생성
val contentFileName = generateFileName(prefix = "${audioContent.id}-content")
// 콘텐츠 파일 업로드
metadata = ObjectMetadata()
metadata.contentLength = contentFile.size
metadata.addUserMetadata(
"generate_preview",
if (request.price > 0) {
request.isGeneratePreview.toString()
} else {
"false"
}
)
if (request.previewStartTime != null && request.previewEndTime != null) {
metadata.addUserMetadata("preview_start_time", request.previewStartTime)
metadata.addUserMetadata("preview_end_time", request.previewEndTime)
}
val contentPath = s3Uploader.upload(
inputStream = contentFile.inputStream,
bucket = audioContentBucket,
filePath = "input/${audioContent.id}/$contentFileName",
metadata = metadata
)
audioContent.content = contentPath
return CreateAudioContentResponse(contentId = audioContent.id!!)
}
private fun validatePreviewTime(previewStartTime: String?, previewEndTime: String?) {
if (previewStartTime != null && previewEndTime != null) {
val startTimeArray = previewStartTime.split(":")
if (startTimeArray.size != 3) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
}
for (time in startTimeArray) {
if (time.length != 2) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
}
}
val endTimeArray = previewEndTime.split(":")
if (endTimeArray.size != 3) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
}
for (time in endTimeArray) {
if (time.length != 2) {
throw SodaException("미리 듣기 시간 형식은 00:30:00 과 같아야 합니다")
}
}
val timeDifference = timeDifference(previewStartTime, previewEndTime)
if (timeDifference < 15000) {
throw SodaException("미리 듣기의 최소 시간은 15초 입니다.")
}
} else {
if (previewStartTime != null || previewEndTime != null) {
throw SodaException("미리 듣기 시작 시간과 종료 시간 둘 다 입력을 하거나 둘 다 입력 하지 않아야 합니다.")
}
}
}
private fun timeDifference(startTime: String, endTime: String): Long {
try {
// Define a date format for parsing the times
val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.KOREAN)
// Parse the input times into Date objects
val date1 = dateFormat.parse(startTime)
val date2 = dateFormat.parse(endTime)
// Check if either date is null
if (date1 == null || date2 == null) {
return 0
}
// Check if the time difference is greater than 30 seconds (30000 milliseconds)
return date2.time - date1.time
} catch (e: Exception) {
// Handle invalid time formats or parsing errors
return 0
}
}
@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.content = content
audioContent.duration = duration
applicationEventPublisher.publishEvent(
FcmEvent(
type = FcmEventType.INDIVIDUAL,
title = "콘텐츠 등록완료",
message = audioContent.title,
recipients = listOf(audioContent.member!!.id!!),
isAuth = null,
contentId = contentId
)
)
if (audioContent.releaseDate == null || audioContent.releaseDate!! <= audioContent.createdAt) {
audioContent.isActive = true
applicationEventPublisher.publishEvent(
FcmEvent(
type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}",
isAuth = audioContent.isAdult,
contentId = contentId,
creatorId = audioContent.member!!.id,
container = "ios"
)
)
applicationEventPublisher.publishEvent(
FcmEvent(
type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}",
isAuth = audioContent.isAdult,
contentId = contentId,
creatorId = audioContent.member!!.id,
container = "aos"
)
)
}
}
@AudioContentReleaseSchedulerOnly
@Transactional
fun releaseContent() {
val notReleasedAudioContent = repository.getNotReleaseContent()
for (audioContent in notReleasedAudioContent) {
audioContent.isActive = true
applicationEventPublisher.publishEvent(
FcmEvent(
type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}",
isAuth = audioContent.isAdult,
contentId = audioContent.id!!,
creatorId = audioContent.member!!.id,
container = "ios"
)
)
applicationEventPublisher.publishEvent(
FcmEvent(
type = FcmEventType.UPLOAD_CONTENT,
title = audioContent.member!!.nickname,
message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}",
isAuth = audioContent.isAdult,
contentId = audioContent.id!!,
creatorId = audioContent.member!!.id,
container = "aos"
)
)
}
}
fun getDetail(id: Long, member: Member, timezone: String): GetAudioContentDetailResponse {
// 오디오 콘텐츠 조회 (content_id, 제목, 내용, 테마, 태그, 19여부, 이미지, 콘텐츠 PATH)
val audioContent = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
// 크리에이터(유저) 정보
val creatorId = audioContent.member!!.id!!
val creator = explorerQueryRepository.getMember(creatorId)
?: throw SodaException("없는 사용자 입니다.")
val creatorFollowing = explorerQueryRepository.getCreatorFollowing(
creatorId = creatorId,
memberId = member.id!!
)
val (isExistsAudioContent, orderType) = orderRepository.isExistOrderedAndOrderType(
memberId = member.id!!,
contentId = audioContent.id!!
)
// 차단된 사용자 체크
val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = creatorId)
if (isBlocked && !isExistsAudioContent) throw SodaException("${creator.nickname}님의 요청으로 콘텐츠 접근이 제한됩니다.")
val orderSequence = if (isExistsAudioContent) {
limitedEditionOrderRepository.getOrderSequence(
contentId = audioContent.id!!,
memberId = member.id!!
)
} else {
null
}
if (
!isExistsAudioContent &&
!audioContent.isActive &&
audioContent.releaseDate != null &&
audioContent.releaseDate!! < LocalDateTime.now()
) {
throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
}
// 댓글
val commentList = if (audioContent.isCommentAvailable) {
commentRepository.findByContentId(
cloudFrontHost = coverImageHost,
contentId = audioContent.id!!,
memberId = member.id!!,
isContentCreator = creatorId == member.id!!,
timezone = timezone,
offset = 0,
limit = 1
)
} else {
listOf()
}
// 댓글 수
val commentCount = if (audioContent.isCommentAvailable) {
commentRepository.totalCountCommentByContentId(
contentId = audioContent.id!!,
memberId = member.id!!,
isContentCreator = creatorId == member.id!!
)
} else {
0
}
val releaseDate = if (
audioContent.releaseDate != null &&
audioContent.releaseDate!! >= LocalDateTime.now()
) {
audioContent.releaseDate!!
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.toLocalDateTime()
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 오픈예정"))
} else {
null
}
val audioContentUrl = if (
audioContent.releaseDate == null ||
audioContent.releaseDate!! <= LocalDateTime.now() ||
creatorId == member.id!!
) {
audioContentCloudFront.generateSignedURL(
resourcePath = if (
isExistsAudioContent ||
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)
)
} else {
""
}
val tag = audioContent.audioContentHashTags
.filter { it.isActive }
.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
}
val pinContent = pinContentRepository.findByContentIdAndMemberId(
contentId = id,
memberId = member.id!!,
active = true
)
val isPin = if (member.id!! == audioContent.member!!.id!!) {
pinContent != null
} else {
false
}
val pinContentListCount = pinContentRepository.getPinContentList(memberId = member.id!!, active = true).size
val isAvailablePin = if (member.id!! == audioContent.member!!.id!!) {
pinContentListCount < 3
} else {
false
}
val isOnlyRental = if (audioContent.purchaseOption == PurchaseOption.RENT_ONLY) {
true
} else {
audioContent.isOnlyRental
}
val purchaseOption = if (audioContent.isOnlyRental) {
PurchaseOption.RENT_ONLY
} else {
audioContent.purchaseOption
}
val contentDetail = if (
audioContent.price >= 50 &&
!isExistsAudioContent &&
!audioContent.isFullDetailVisible
) {
val length = audioContent.detail.length
if (length < 60) {
"${audioContent.detail.take(length / 2)}..."
} else {
"${audioContent.detail.take(30)}..."
}
} else {
audioContent.detail
}
return GetAudioContentDetailResponse(
contentId = audioContent.id!!,
title = audioContent.title,
detail = contentDetail,
coverImageUrl = "$coverImageHost/${audioContent.coverImage!!}",
contentUrl = audioContentUrl,
themeStr = audioContent.theme!!.theme,
tag = tag,
price = audioContent.price,
duration = audioContent.duration ?: "",
releaseDate = releaseDate,
totalContentCount = audioContent.limited,
remainingContentCount = audioContent.remaining,
orderSequence = orderSequence,
isActivePreview = audioContent.isGeneratePreview,
isAdult = audioContent.isAdult,
isMosaic = audioContent.isAdult && member.auth == null,
isOnlyRental = isOnlyRental,
existOrdered = isExistsAudioContent,
purchaseOption = purchaseOption,
orderType = orderType,
remainingTime = remainingTime,
creatorOtherContentList = creatorOtherContentList,
sameThemeOtherContentList = sameThemeOtherContentList,
isCommentAvailable = audioContent.isCommentAvailable,
isLike = isLike,
likeCount = likeCount,
commentList = commentList,
commentCount = commentCount,
isPin = isPin,
isAvailablePin = isAvailablePin,
creator = AudioContentCreator(
creatorId = creatorId,
nickname = creator.nickname,
profileImageUrl = if (creator.profileImage != null) {
"$coverImageHost/${creator.profileImage}"
} else {
"$coverImageHost/profile/default-profile.png"
},
isFollowing = creatorFollowing?.isFollow ?: false,
isFollow = creatorFollowing?.isFollow ?: false,
isNotify = creatorFollowing?.isNotify ?: false
)
)
}
fun getAudioContentList(
creatorId: Long,
sortType: SortType,
member: Member,
categoryId: Long = 0,
offset: Long,
limit: Long
): GetAudioContentListResponse {
val totalCount = repository.findTotalCountByCreatorId(
creatorId = creatorId,
isAdult = member.auth != null,
categoryId = categoryId
)
val audioContentList = repository.findByCreatorId(
creatorId = creatorId,
coverImageHost = coverImageHost,
isAdult = member.auth != null,
sortType = sortType,
categoryId = categoryId,
offset = offset,
limit = limit
)
val items = audioContentList
.map {
val commentCount = commentRepository
.totalCountCommentByContentId(
it.contentId,
memberId = member.id!!,
isContentCreator = creatorId == member.id!!
)
it.commentCount = commentCount
val likeCount = audioContentLikeRepository
.totalCountAudioContentLike(it.contentId)
it.likeCount = likeCount
val (isExistsAudioContent, orderType) = orderRepository.isExistOrderedAndOrderType(
memberId = member.id!!,
contentId = it.contentId
)
if (isExistsAudioContent) {
if (orderType == OrderType.RENTAL) {
it.isRented = true
} else {
it.isOwned = true
}
}
it
}
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
)
)
}
}
@Transactional(readOnly = true)
@Cacheable(
cacheNames = ["cache_ttl_3_days"],
key = "'contentRanking:' + ':' +" +
"#isAdult + ':' + #startDate + ':' + #endDate + ':' + #sortType + ':' + #offset + ':' + #limit"
)
fun getAudioContentRanking(
isAdult: Boolean,
startDate: LocalDateTime,
endDate: LocalDateTime,
offset: Long,
limit: Long,
sortType: String = "매출"
): GetAudioContentRanking {
val startDateFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일")
val endDateFormatter = DateTimeFormatter.ofPattern("MM월 dd일")
val contentRankingItemList = repository
.getAudioContentRanking(
cloudfrontHost = coverImageHost,
startDate = startDate.minusDays(1),
endDate = endDate.minusDays(1),
isAdult = isAdult,
offset = offset,
limit = limit,
sortType = sortType
)
return GetAudioContentRanking(
startDate = startDate.format(startDateFormatter),
endDate = endDate.minusDays(1).format(endDateFormatter),
items = contentRankingItemList
)
}
fun getContentRankingSortTypeList(): List<String> {
return listOf("매출", "댓글", "좋아요")
}
@Transactional
fun pinToTheTop(contentId: Long, member: Member) {
val audioContent = repository.findByIdAndCreatorId(contentId = contentId, creatorId = member.id!!)
?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
if (audioContent.releaseDate != null && audioContent.releaseDate!! >= LocalDateTime.now()) {
throw SodaException("콘텐츠 오픈 후 채널에 고정이 가능합니다.")
}
var pinContent = pinContentRepository.findByContentIdAndMemberId(
contentId = contentId,
memberId = member.id!!
)
if (pinContent != null) {
pinContent.isActive = true
} else {
val pinContentList = pinContentRepository.getPinContentList(memberId = member.id!!)
pinContent = if (pinContentList.size >= 3) {
pinContentList[0]
} else {
PinContent()
}
pinContent.isActive = true
pinContent.member = member
pinContent.content = audioContent
pinContentRepository.save(pinContent)
}
}
@Transactional
fun unpinAtTheTop(contentId: Long, member: Member) {
val pinContent = pinContentRepository.findByContentIdAndMemberId(
contentId = contentId,
memberId = member.id!!
) ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.")
pinContent.isActive = false
}
}