| @@ -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") | ||||
|   | ||||
| @@ -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") | ||||
|     } | ||||
| } | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
| @@ -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( | ||||
|   | ||||
| @@ -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<AudioContentCuration> | ||||
|  | ||||
|     fun getNotReleaseContentId(): List<Long> | ||||
|     fun getNotReleaseContent(): List<AudioContent> | ||||
|  | ||||
|     fun isContentCreator(contentId: Long, memberId: Long): Boolean | ||||
|  | ||||
|     fun fetchContentForPlaylist(contentIdList: List<Long>): List<AudioContentPlaylistContent> | ||||
|     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<Long> { | ||||
|     override fun getNotReleaseContent(): List<AudioContent> { | ||||
|         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<Long>): List<AudioContentPlaylistContent> { | ||||
|         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() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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" | ||||
|                 ) | ||||
|   | ||||
| @@ -35,6 +35,8 @@ interface OrderQueryRepository { | ||||
|         offset: Long = 0, | ||||
|         limit: Long = 20 | ||||
|     ): List<GetAudioContentMainItem> | ||||
|  | ||||
|     fun findOrderedContent(contentIdList: List<Long>, memberId: Long): List<Long> | ||||
| } | ||||
|  | ||||
| @Repository | ||||
| @@ -218,4 +220,19 @@ class OrderQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Orde | ||||
|             .orderBy(order.createdAt.desc()) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     override fun findOrderedContent(contentIdList: List<Long>, memberId: Long): List<Long> { | ||||
|         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() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<PlaylistContentIdAndOrder>, | ||||
|  | ||||
|     // 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 | ||||
| ) | ||||
| @@ -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)) | ||||
|     } | ||||
| } | ||||
| @@ -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<AudioContentPlaylist, Long> { | ||||
|     fun findByMemberId(memberId: Long): List<AudioContentPlaylist> | ||||
| } | ||||
| @@ -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<Long>, 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" | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,7 @@ | ||||
| package kr.co.vividnext.sodalive.content.playlist | ||||
|  | ||||
| data class CreatePlaylistRequest( | ||||
|     val title: String, | ||||
|     val desc: String? = null, | ||||
|     val contentIdAndOrderList: List<PlaylistContentIdAndOrder> = emptyList() | ||||
| ) | ||||
| @@ -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<String>, | ||||
|     val contentList: List<AudioContentPlaylistContent> | ||||
| ) | ||||
| @@ -0,0 +1,14 @@ | ||||
| package kr.co.vividnext.sodalive.content.playlist | ||||
|  | ||||
| data class GetPlaylistsResponse( | ||||
|     val totalCount: Int, | ||||
|     val items: List<GetPlaylistsItem> | ||||
| ) | ||||
|  | ||||
| data class GetPlaylistsItem( | ||||
|     val id: Long, | ||||
|     val title: String, | ||||
|     val desc: String, | ||||
|     val contentCount: Int, | ||||
|     val coverImageUrl: String | ||||
| ) | ||||
| @@ -0,0 +1,6 @@ | ||||
| package kr.co.vividnext.sodalive.content.playlist | ||||
|  | ||||
| data class PlaylistContentIdAndOrder( | ||||
|     val contentId: Long, | ||||
|     val order: Int | ||||
| ) | ||||
| @@ -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<PlaylistContentIdAndOrder> = emptyList() | ||||
| ) | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user