diff --git a/build.gradle.kts b/build.gradle.kts index 2d3a3e5..b0ee365 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,7 @@ repositories { } dependencies { + implementation("org.springframework.boot:spring-boot-starter-aop") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-data-redis") implementation("org.springframework.boot:spring-boot-starter-security") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/annotation/AudioContentReleaseSchedulerOnly.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/annotation/AudioContentReleaseSchedulerOnly.kt new file mode 100644 index 0000000..28340bc --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/annotation/AudioContentReleaseSchedulerOnly.kt @@ -0,0 +1,26 @@ +package kr.co.vividnext.sodalive.common.annotation + +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before +import org.springframework.stereotype.Component + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class AudioContentReleaseSchedulerOnly + +@Aspect +@Component +class SchedulerOnlyAspect { + + @Before("@annotation(AudioContentReleaseSchedulerOnly)") + fun checkSchedulerAccess() { + if (!isSchedulerThread()) { + throw IllegalStateException("잘못된 접근입니다.") + } + } + + private fun isSchedulerThread(): Boolean { + // 스케줄러 스레드 여부를 판단하는 간단한 로직 + return Thread.currentThread().name.contains("AudioContentRelease-Scheduler") + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SchedulerConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SchedulerConfig.kt new file mode 100644 index 0000000..09cbe14 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SchedulerConfig.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.configs + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary +import org.springframework.scheduling.annotation.EnableScheduling +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler + +@Configuration +@EnableScheduling +class SchedulerConfig { + @Primary + @Bean(name = ["taskScheduler"]) + fun taskScheduler(): ThreadPoolTaskScheduler { + val scheduler = ThreadPoolTaskScheduler() + scheduler.poolSize = 5 + scheduler.setThreadNamePrefix("DefaultScheduler-") + scheduler.setWaitForTasksToCompleteOnShutdown(true) // 종료 시 대기 설정 + scheduler.setAwaitTerminationSeconds(10) // 최대 10초 대기 + return scheduler + } + + @Bean(name = ["audioContentReleaseScheduler"]) + fun audioContentReleaseScheduler(): ThreadPoolTaskScheduler { + val scheduler = ThreadPoolTaskScheduler() + scheduler.poolSize = 2 + scheduler.setThreadNamePrefix("AudioContentRelease-Scheduler-") + scheduler.setWaitForTasksToCompleteOnShutdown(true) // 종료 시 대기 설정 + scheduler.setAwaitTerminationSeconds(10) // 최대 10초 대기 + return scheduler + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt index 750a882..2a2a983 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt @@ -193,12 +193,6 @@ class AudioContentController(private val service: AudioContentService) { ) } - @PostMapping("/release") - @PreAuthorize("hasRole('BOT')") - fun releaseContent() = run { - ApiResponse.ok(service.releaseContent()) - } - @PostMapping("/pin-to-the-top/{id}") @PreAuthorize("hasRole('CREATOR')") fun pinToTheTop( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt index 979ceb8..628d5b3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -17,10 +17,13 @@ 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.order.QOrder.order import kr.co.vividnext.sodalive.content.pin.QPinContent.pinContent +import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylistContent +import kr.co.vividnext.sodalive.content.playlist.QAudioContentPlaylistContent import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme import kr.co.vividnext.sodalive.event.QEvent.event import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.QMember.member +import org.springframework.beans.factory.annotation.Value import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository import java.time.LocalDateTime @@ -117,13 +120,21 @@ interface AudioContentQueryRepository { fun getAudioContentCurationList(isAdult: Boolean, offset: Long, limit: Long): List - fun getNotReleaseContentId(): List + fun getNotReleaseContent(): List fun isContentCreator(contentId: Long, memberId: Long): Boolean + + fun fetchContentForPlaylist(contentIdList: List): List + fun getCoverImageById(id: Long): String? } @Repository -class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AudioContentQueryRepository { +class AudioContentQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory, + + @Value("\${cloud.aws.cloud-front.host}") + private val imageHost: String +) : AudioContentQueryRepository { override fun findByIdAndActive(contentId: Long): AudioContent? { return queryFactory .selectFrom(audioContent) @@ -751,15 +762,14 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) .fetch() } - override fun getNotReleaseContentId(): List { + override fun getNotReleaseContent(): List { val where = audioContent.isActive.isFalse .and(audioContent.releaseDate.isNotNull) .and(audioContent.releaseDate.loe(LocalDateTime.now())) .and(audioContent.duration.isNotNull) return queryFactory - .select(audioContent.id) - .from(audioContent) + .selectFrom(audioContent) .where(where) .fetch() } @@ -778,4 +788,27 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) return foundedContentId != null && foundedContentId == contentId } + + override fun fetchContentForPlaylist(contentIdList: List): List { + return queryFactory + .select( + QAudioContentPlaylistContent( + audioContent.id, + audioContent.title, + audioContentTheme.theme, + audioContent.coverImage.prepend("/").prepend(imageHost), + audioContent.duration + ) + ) + .from(audioContent) + .innerJoin(audioContent.theme, audioContentTheme) + .fetch() + } + + override fun getCoverImageById(id: Long): String? { + return queryFactory + .select(audioContent.coverImage.prepend("/").prepend(imageHost)) + .from(audioContent) + .fetchFirst() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index d971e56..05f9738 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -5,6 +5,7 @@ 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 @@ -402,14 +403,12 @@ class AudioContentService( } } + @AudioContentReleaseSchedulerOnly @Transactional fun releaseContent() { - val contentIdList = repository.getNotReleaseContentId() - - for (contentId in contentIdList) { - val audioContent = repository.findByIdOrNull(contentId) - ?: throw SodaException("잘못된 요청입니다.") + val notReleasedAudioContent = repository.getNotReleaseContent() + for (audioContent in notReleasedAudioContent) { audioContent.isActive = true applicationEventPublisher.publishEvent( @@ -418,7 +417,7 @@ class AudioContentService( title = audioContent.member!!.nickname, message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}", isAuth = audioContent.isAdult, - contentId = contentId, + contentId = audioContent.id!!, creatorId = audioContent.member!!.id, container = "ios" ) @@ -430,7 +429,7 @@ class AudioContentService( title = audioContent.member!!.nickname, message = "콘텐츠를 업로드 하였습니다. - ${audioContent.title}", isAuth = audioContent.isAdult, - contentId = contentId, + contentId = audioContent.id!!, creatorId = audioContent.member!!.id, container = "aos" ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRepository.kt index 42ede81..fb6022a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRepository.kt @@ -35,6 +35,8 @@ interface OrderQueryRepository { offset: Long = 0, limit: Long = 20 ): List + + fun findOrderedContent(contentIdList: List, memberId: Long): List } @Repository @@ -218,4 +220,19 @@ class OrderQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Orde .orderBy(order.createdAt.desc()) .fetch() } + + override fun findOrderedContent(contentIdList: List, memberId: Long): List { + return queryFactory + .select(audioContent.id) + .from(order) + .innerJoin(order.member, member) + .innerJoin(order.audioContent, audioContent) + .where( + order.isActive.isTrue, + order.endDate.isNull, + member.id.eq(memberId), + audioContent.id.`in`(contentIdList) + ) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylist.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylist.kt new file mode 100644 index 0000000..d9dd2ec --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylist.kt @@ -0,0 +1,33 @@ +package kr.co.vividnext.sodalive.content.playlist + +import com.querydsl.core.annotations.QueryProjection +import org.springframework.data.annotation.Id +import org.springframework.data.redis.core.RedisHash +import org.springframework.data.redis.core.index.Indexed +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +@RedisHash("AudioContentPlaylist") +data class AudioContentPlaylist( + @Id + val id: Long, + @Indexed + val memberId: Long, + var title: String, + var desc: String? = null, + var contentIdAndOrderList: List, + + // ISO 8601 형식의 String + private val _createdAt: String = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) +) { + val createdAt: LocalDateTime + get() = LocalDateTime.parse(_createdAt, DateTimeFormatter.ISO_LOCAL_DATE_TIME) +} + +data class AudioContentPlaylistContent @QueryProjection constructor( + val id: Long, + val title: String, + val category: String, + val coverUrl: String, + val duration: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylistController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylistController.kt new file mode 100644 index 0000000..d368441 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylistController.kt @@ -0,0 +1,68 @@ +package kr.co.vividnext.sodalive.content.playlist + +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.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.RestController + +@RestController +@RequestMapping("/audio-content/playlist") +class AudioContentPlaylistController(private val service: AudioContentPlaylistService) { + @PostMapping + fun createPlaylist( + @RequestBody request: CreatePlaylistRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.createPlaylist(request, member)) + } + + @PutMapping("/{id}") + fun updatePlaylist( + @PathVariable id: Long, + @RequestBody request: UpdatePlaylistRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.updatePlaylist(playlistId = id, request = request, member = member)) + } + + @DeleteMapping("/{id}") + fun deletePlaylist( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.deletePlaylist(playlistId = id, member)) + } + + @GetMapping + fun getPlaylists( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getPlaylists(member)) + } + + @GetMapping("/{id}") + fun getPlaylistDetail( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getPlaylistDetail(playlistId = id, member = member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylistRedisRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylistRedisRepository.kt new file mode 100644 index 0000000..6dedba0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylistRedisRepository.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.content.playlist + +import org.springframework.data.repository.CrudRepository +import org.springframework.stereotype.Repository + +@Repository +interface AudioContentPlaylistRedisRepository : CrudRepository { + fun findByMemberId(memberId: Long): List +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylistService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylistService.kt new file mode 100644 index 0000000..4bc76ce --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/AudioContentPlaylistService.kt @@ -0,0 +1,156 @@ +package kr.co.vividnext.sodalive.content.playlist + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.AudioContentRepository +import kr.co.vividnext.sodalive.content.order.OrderRepository +import kr.co.vividnext.sodalive.live.roulette.RedisIdGenerator +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +class AudioContentPlaylistService( + private val idGenerator: RedisIdGenerator, + private val orderRepository: OrderRepository, + private val audioContentRepository: AudioContentRepository, + private val redisRepository: AudioContentPlaylistRedisRepository +) { + fun createPlaylist(request: CreatePlaylistRequest, member: Member) { + if (request.contentIdAndOrderList.size >= 30) { + throw SodaException("플레이 리스트에는 최대 30개의 콘텐츠를 저장할 수 있습니다.") + } + + val playlistCount = redisRepository.findByMemberId(member.id!!).size + if (playlistCount >= 10) { + throw SodaException("플레이 리스트는 최대 10개까지 생성할 수 있습니다.") + } + + // 콘텐츠 유효성 검사 (소장으로 구매한 콘텐츠 인가?) + checkOrderedContent( + contentIdList = request.contentIdAndOrderList.map { it.contentId }, + memberId = member.id!! + ) + + val playlist = AudioContentPlaylist( + id = idGenerator.generateId(SEQUENCE_NAME), + memberId = member.id!!, + title = request.title, + desc = request.desc, + contentIdAndOrderList = request.contentIdAndOrderList + ) + + redisRepository.save(playlist) + } + + private fun checkOrderedContent(contentIdList: List, memberId: Long) { + val orderedContentIdList = orderRepository.findOrderedContent(contentIdList, memberId).toSet() + val orderedContentMap = contentIdList.associateWith { it in orderedContentIdList } + val notOrderedContentList = orderedContentMap.filterValues { !it }.keys + + if (notOrderedContentList.isNotEmpty()) { + throw SodaException("소장하지 않은 콘텐츠는 재생목록에 추가할 수 없습니다.") + } + } + + fun updatePlaylist(playlistId: Long, request: UpdatePlaylistRequest, member: Member) { + if (request.contentIdAndOrderList.size >= 30) { + throw SodaException("플레이 리스트에는 최대 30개의 콘텐츠를 저장할 수 있습니다.") + } + + val playlist = redisRepository.findByIdOrNull(id = playlistId) + ?: throw SodaException("잘못된 요청입니다.") + + if (playlist.memberId != member.id) { + throw SodaException("잘못된 요청입니다.") + } + + checkOrderedContent( + contentIdList = request.contentIdAndOrderList.map { it.contentId }, + memberId = member.id!! + ) + + val updatePlaylist = playlist.copy( + title = request.title ?: playlist.title, + desc = request.desc ?: playlist.desc, + contentIdAndOrderList = request.contentIdAndOrderList + ) + + redisRepository.save(updatePlaylist) + } + + fun getPlaylists(member: Member): GetPlaylistsResponse { + val playlists = redisRepository.findByMemberId(memberId = member.id!!) + + return GetPlaylistsResponse( + totalCount = playlists.size, + items = playlists.map { + val contentCount = it.contentIdAndOrderList.size + val coverImageUrl = if (contentCount > 0) { + audioContentRepository.getCoverImageById(id = it.contentIdAndOrderList[0].contentId) + ?: "" + } else { + "" + } + GetPlaylistsItem( + id = it.id, + title = it.title, + desc = it.desc ?: "", + contentCount = contentCount, + coverImageUrl = coverImageUrl + ) + } + ) + } + + fun deletePlaylist(playlistId: Long, member: Member) { + val playlist = redisRepository.findByIdOrNull(id = playlistId) + ?: throw SodaException("잘못된 요청입니다.") + + if (playlist.memberId != member.id) { + throw SodaException("잘못된 요청입니다.") + } + + redisRepository.delete(playlist) + } + + fun getPlaylistDetail(playlistId: Long, member: Member): GetPlaylistDetailResponse { + val playlist = redisRepository.findByIdOrNull(id = playlistId) + ?: throw SodaException("잘못된 요청입니다.") + + if (playlist.memberId != member.id) { + throw SodaException("잘못된 요청입니다.") + } + + val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + val createdDate = playlist.createdAt + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of("Asia/Seoul")) + .format(dateTimeFormatter) + + val contentList = audioContentRepository.fetchContentForPlaylist( + contentIdList = playlist.contentIdAndOrderList.map { it.contentId } + ) + + val orderMap = playlist.contentIdAndOrderList.sortedBy { it.order } + .mapIndexed { index, item -> item.contentId to index } + .toMap() + + val sortedContentList = contentList.sortedBy { orderMap[it.id] } + + return GetPlaylistDetailResponse( + playlistId = playlist.id, + title = playlist.title, + desc = playlist.desc ?: "", + createdDate = createdDate, + contentCount = sortedContentList.size, + playlistCoverImageList = sortedContentList.take(4).map { it.coverUrl }, + contentList = sortedContentList + ) + } + + companion object { + const val SEQUENCE_NAME = "AudioContentPlaylist:sequence" + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/CreatePlaylistRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/CreatePlaylistRequest.kt new file mode 100644 index 0000000..1cdc09e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/CreatePlaylistRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.content.playlist + +data class CreatePlaylistRequest( + val title: String, + val desc: String? = null, + val contentIdAndOrderList: List = emptyList() +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/GetPlaylistDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/GetPlaylistDetailResponse.kt new file mode 100644 index 0000000..1d89d53 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/GetPlaylistDetailResponse.kt @@ -0,0 +1,11 @@ +package kr.co.vividnext.sodalive.content.playlist + +data class GetPlaylistDetailResponse( + val playlistId: Long, + val title: String, + val desc: String, + val createdDate: String, + val contentCount: Int, + val playlistCoverImageList: List, + val contentList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/GetPlaylistsResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/GetPlaylistsResponse.kt new file mode 100644 index 0000000..b292c01 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/GetPlaylistsResponse.kt @@ -0,0 +1,14 @@ +package kr.co.vividnext.sodalive.content.playlist + +data class GetPlaylistsResponse( + val totalCount: Int, + val items: List +) + +data class GetPlaylistsItem( + val id: Long, + val title: String, + val desc: String, + val contentCount: Int, + val coverImageUrl: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/PlaylistContentIdAndOrder.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/PlaylistContentIdAndOrder.kt new file mode 100644 index 0000000..2e0d558 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/PlaylistContentIdAndOrder.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.content.playlist + +data class PlaylistContentIdAndOrder( + val contentId: Long, + val order: Int +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/UpdatePlaylistRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/UpdatePlaylistRequest.kt new file mode 100644 index 0000000..2c739c1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/playlist/UpdatePlaylistRequest.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.content.playlist + +data class UpdatePlaylistRequest( + val title: String? = null, + val desc: String? = null, + val contentIdAndOrderList: List = emptyList() +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/scheduler/AudioContentReleaseScheduledTask.kt b/src/main/kotlin/kr/co/vividnext/sodalive/scheduler/AudioContentReleaseScheduledTask.kt new file mode 100644 index 0000000..b07c6fe --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/scheduler/AudioContentReleaseScheduledTask.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.scheduler + +import kr.co.vividnext.sodalive.content.AudioContentService +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler +import org.springframework.scheduling.support.CronTrigger +import org.springframework.stereotype.Component +import java.util.concurrent.ScheduledFuture +import javax.annotation.PostConstruct +import javax.annotation.PreDestroy + +@Component +class AudioContentReleaseScheduledTask( + private val audioContentService: AudioContentService, + @Qualifier("audioContentReleaseScheduler") private val audioContentReleaseScheduler: ThreadPoolTaskScheduler +) { + private var scheduledTask: ScheduledFuture<*>? = null + + @PostConstruct + fun release() { + scheduledTask = audioContentReleaseScheduler.schedule( + { audioContentService.releaseContent() }, + CronTrigger("0 0/15 * * * *") + ) + } + + @PreDestroy + fun stopReleaseScheduler() { + scheduledTask?.cancel(false) + audioContentReleaseScheduler.shutdown() + } +}