diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt index a1f5c15..44acb16 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt @@ -5,6 +5,7 @@ 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 java.time.LocalDateTime import javax.persistence.CascadeType import javax.persistence.Column import javax.persistence.Entity @@ -32,6 +33,7 @@ data class AudioContent( @Column(columnDefinition = "TEXT", nullable = false) var detail: String, val price: Int = 0, + var releaseDate: LocalDateTime? = null, @Enumerated(value = EnumType.STRING) val type: AudioContentType = AudioContentType.INDIVIDUAL, val isGeneratePreview: Boolean = true, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt index 537f560..ab678be 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt @@ -25,7 +25,10 @@ import java.time.temporal.TemporalAdjusters @RestController @RequestMapping("/audio-content") -class AudioContentController(private val service: AudioContentService) { +class AudioContentController( + private val service: AudioContentService, + private val repository: AudioContentRepository +) { @PostMapping @PreAuthorize("hasRole('CREATOR')") fun createAudioContent( @@ -190,4 +193,16 @@ class AudioContentController(private val service: AudioContentService) { ) ) } + + @PostMapping("/release") + @PreAuthorize("hasRole('BOT')") + fun releaseContent() = run { + val contentIdList = repository.getNotReleaseContentId() + + for (contentId in contentIdList) { + service.releaseContent(contentId) + } + + ApiResponse.ok(null) + } } 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 f609302..15a1427 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -56,6 +56,7 @@ interface AudioContentQueryRepository { fun findByTheme( cloudfrontHost: String, + memberId: Long, theme: String = "", isAdult: Boolean = false, offset: Long = 0, @@ -64,13 +65,14 @@ interface AudioContentQueryRepository { fun findByThemeFor2Weeks( cloudfrontHost: String, + memberId: Long, theme: String = "", isAdult: Boolean = false, offset: Long = 0, limit: Long = 20 ): List - fun totalCountNewContentFor2Weeks(theme: String, isAdult: Boolean): Int + fun totalCountNewContentFor2Weeks(theme: String, memberId: Long, isAdult: Boolean): Int fun getNewContentUploadCreatorList( cloudfrontHost: String, @@ -96,6 +98,8 @@ interface AudioContentQueryRepository { ): List fun getAudioContentCurationList(isAdult: Boolean, offset: Long, limit: Long): List + + fun getNotReleaseContentId(): List } @Repository @@ -146,8 +150,11 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) SortType.PRICE_LOW -> audioContent.price.asc() } - var where = audioContent.isActive.isTrue - .and(audioContent.member.id.eq(creatorId)) + var where = audioContent.member.id.eq(creatorId) + .and( + audioContent.isActive.isTrue + .or(audioContent.releaseDate.isNotNull.and(audioContent.duration.isNotNull)) + ) if (!isAdult) { where = where.and(audioContent.isAdult.isFalse) @@ -166,8 +173,11 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) creatorId: Long, isAdult: Boolean ): Int { - var where = audioContent.isActive.isTrue - .and(audioContent.member.id.eq(creatorId)) + var where = audioContent.member.id.eq(creatorId) + .and( + audioContent.isActive.isTrue + .or(audioContent.releaseDate.isNotNull.and(audioContent.duration.isNotNull)) + ) if (!isAdult) { where = where.and(audioContent.isAdult.isFalse) @@ -242,12 +252,18 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) override fun findByTheme( cloudfrontHost: String, + memberId: Long, theme: String, isAdult: Boolean, offset: Long, limit: Long ): List { var where = audioContent.isActive.isTrue + .and( + audioContent.releaseDate.isNull + .or(audioContent.releaseDate.loe(LocalDateTime.now())) + .or(audioContent.member.id.eq(memberId)) + ) if (!isAdult) { where = where.and(audioContent.isAdult.isFalse) @@ -278,9 +294,14 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) .fetch() } - override fun totalCountNewContentFor2Weeks(theme: String, isAdult: Boolean): Int { + override fun totalCountNewContentFor2Weeks(theme: String, memberId: Long, isAdult: Boolean): Int { var where = audioContent.isActive.isTrue .and(audioContent.createdAt.goe(LocalDateTime.now().minusWeeks(2))) + .and( + audioContent.releaseDate.isNull + .or(audioContent.releaseDate.loe(LocalDateTime.now())) + .or(audioContent.member.id.eq(memberId)) + ) if (!isAdult) { where = where.and(audioContent.isAdult.isFalse) @@ -302,6 +323,7 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) override fun findByThemeFor2Weeks( cloudfrontHost: String, + memberId: Long, theme: String, isAdult: Boolean, offset: Long, @@ -309,6 +331,11 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) ): List { var where = audioContent.isActive.isTrue .and(audioContent.createdAt.goe(LocalDateTime.now().minusWeeks(2))) + .and( + audioContent.releaseDate.isNull + .or(audioContent.releaseDate.loe(LocalDateTime.now())) + .or(audioContent.member.id.eq(memberId)) + ) if (!isAdult) { where = where.and(audioContent.isAdult.isFalse) @@ -562,4 +589,16 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) .orderBy(audioContentCuration.orders.asc()) .fetch() } + + override fun getNotReleaseContentId(): List { + val where = audioContent.isActive.isFalse + .and(audioContent.releaseDate.isNotNull) + .and(audioContent.releaseDate.loe(LocalDateTime.now())) + + return queryFactory + .select(audioContent.id) + .from(audioContent) + .where(where) + .fetch() + } } 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 b0f3079..90b0b54 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -18,6 +18,7 @@ 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.extensions.convertLocalDateTime import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.member.Member @@ -125,6 +126,7 @@ class AudioContentService( ?: throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") audioContent.isActive = false + audioContent.releaseDate = null } @Transactional @@ -143,6 +145,15 @@ class AudioContentService( // 미리듣기 시간 체크 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 { + null + } + // contentFile 체크 if (contentFile == null && request.type == AudioContentType.INDIVIDUAL) { throw SodaException("콘텐츠를 선택해 주세요.") @@ -168,6 +179,7 @@ class AudioContentService( } else { 0 }, + releaseDate = releaseDate, isAdult = request.isAdult, isGeneratePreview = if (request.type == AudioContentType.INDIVIDUAL) { request.isGeneratePreview @@ -331,7 +343,6 @@ class AudioContentService( val audioContent = repository.findByIdOrNull(contentId) ?: throw SodaException("잘못된 요청입니다.") - audioContent.isActive = true audioContent.content = content audioContent.duration = duration @@ -346,6 +357,42 @@ class AudioContentService( ) ) + if (audioContent.releaseDate == null) { + 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" + ) + ) + } + } + + @Transactional + fun releaseContent(contentId: Long) { + val audioContent = repository.findByIdOrNull(contentId) + ?: throw SodaException("잘못된 요청입니다.") + + audioContent.isActive = true + applicationEventPublisher.publishEvent( FcmEvent( type = FcmEventType.UPLOAD_CONTENT, @@ -401,7 +448,12 @@ class AudioContentService( contentId = audioContent.id!! ) - if (!isExistsAudioContent && !isExistsBundleAudioContent && !audioContent.isActive) { + if ( + !isExistsAudioContent && + !isExistsBundleAudioContent && + !audioContent.isActive && + audioContent.releaseDate == null + ) { throw SodaException("잘못된 콘텐츠 입니다.\n다시 시도해 주세요.") } @@ -425,19 +477,40 @@ class AudioContentService( 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 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 || + 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) + ) + } else { + "" + } val tag = audioContent.audioContentHashTags .map { it.hashTag!!.tag } @@ -483,6 +556,7 @@ class AudioContentService( tag = tag, price = audioContent.price, duration = audioContent.duration ?: "", + releaseDate = releaseDate, isAdult = audioContent.isAdult, isMosaic = audioContent.isAdult && member.auth == null, isOnlyRental = audioContent.isOnlyRental, @@ -546,7 +620,8 @@ class AudioContentService( duration = it.duration, likeCount = likeCount, commentCount = commentCount, - isAdult = it.isAdult + isAdult = it.isAdult, + isScheduledToOpen = it.releaseDate != null && it.releaseDate!! > LocalDateTime.now() ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt index 619b27f..e2acdc6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/CreateAudioContentRequest.kt @@ -5,6 +5,8 @@ data class CreateAudioContentRequest( val detail: String, val tags: String, val price: Int, + val timezone: String, + val releaseDate: String? = null, val themeId: Long = 0, val isAdult: Boolean = false, val isGeneratePreview: Boolean = true, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt index 8d0c468..c91248f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentDetailResponse.kt @@ -14,6 +14,7 @@ data class GetAudioContentDetailResponse( val tag: String, val price: Int, val duration: String, + val releaseDate: String?, val isAdult: Boolean, val isMosaic: Boolean, val isOnlyRental: Boolean, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt index bfef003..2986176 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/GetAudioContentListResponse.kt @@ -14,5 +14,6 @@ data class GetAudioContentListItem( val duration: String?, val likeCount: Int, val commentCount: Int, - val isAdult: Boolean + val isAdult: Boolean, + val isScheduledToOpen: Boolean ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt index 4a6ea13..058ccf3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt @@ -15,19 +15,8 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("/audio-content/main") class AudioContentMainController( private val service: AudioContentMainService, - private val orderService: OrderService, - private val manageService: AudioContentMainManageService + private val orderService: OrderService ) { - - @GetMapping - fun getMain( - @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? - ) = run { - if (member == null) throw SodaException("로그인 정보를 확인해주세요.") - - ApiResponse.ok(manageService.getMain(member)) - } - @GetMapping("/new-content-upload-creator") fun newContentUploadCreatorList( @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainManageService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainManageService.kt deleted file mode 100644 index 4ec97e1..0000000 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainManageService.kt +++ /dev/null @@ -1,94 +0,0 @@ -package kr.co.vividnext.sodalive.content.main - -import kr.co.vividnext.sodalive.content.AudioContentRepository -import kr.co.vividnext.sodalive.content.AudioContentService -import kr.co.vividnext.sodalive.content.order.OrderService -import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository -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 -import org.springframework.transaction.annotation.Transactional -import java.time.DayOfWeek -import java.time.LocalDateTime -import java.time.temporal.TemporalAdjusters - -@Service -class AudioContentMainManageService( - private val service: AudioContentMainService, - private val orderService: OrderService, - private val audioContentService: AudioContentService, - - private val repository: AudioContentRepository, - private val blockMemberRepository: BlockMemberRepository, - private val audioContentThemeRepository: AudioContentThemeQueryRepository, - - @Value("\${cloud.aws.cloud-front.host}") - private val imageHost: String -) { - @Transactional(readOnly = true) - fun getMain(member: Member): GetAudioContentMainResponse { - val memberId = member.id!! - val isAdult = member.auth != null - - val newContentUploadCreatorList = service.getNewContentUploadCreatorList( - memberId = memberId, - isAdult = isAdult - ) - - val bannerList = service.getAudioContentMainBannerList(memberId = memberId, isAdult = isAdult) - - val orderList = orderService.getAudioContentMainOrderList( - memberId = memberId, - limit = 20 - ) - - // 콘텐츠 테마 - val themeList = audioContentThemeRepository.getActiveThemeOfContent(isAdult = isAdult) - - // 새 콘텐츠 20개 - 시간 내림차순 정렬 - val newContentList = repository.findByTheme( - cloudfrontHost = imageHost, - isAdult = isAdult - ) - .asSequence() - .filter { - !blockMemberRepository.isBlocked( - blockedMemberId = memberId, - memberId = it.creatorId - ) - } - .toList() - - val curationList = service.getAudioContentCurationList(memberId = memberId, isAdult = isAdult) - - val currentDateTime = LocalDateTime.now() - val startDate = currentDateTime - .minusWeeks(1) - .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) - .withHour(15) - .withMinute(0) - .withSecond(0) - val endDate = startDate.plusDays(7) - - val contentRankingSortTypeList = audioContentService.getContentRankingSortTypeList() - val contentRanking = audioContentService.getAudioContentRanking( - isAdult = isAdult, - startDate = startDate, - endDate = endDate, - offset = 0, - limit = 12 - ) - - return GetAudioContentMainResponse( - newContentUploadCreatorList = newContentUploadCreatorList, - bannerList = bannerList, - orderList = orderList, - themeList = themeList, - newContentList = newContentList, - curationList = curationList, - contentRankingSortTypeList = contentRankingSortTypeList, - contentRanking = contentRanking - ) - } -} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt index 96d4b75..1df8343 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt @@ -33,6 +33,7 @@ class AudioContentMainService( fun getNewContentByTheme(theme: String, member: Member, pageable: Pageable): List { return repository.findByTheme( cloudfrontHost = imageHost, + memberId = member.id!!, theme = theme, isAdult = member.auth != null, offset = pageable.offset, @@ -45,9 +46,11 @@ class AudioContentMainService( @Transactional(readOnly = true) fun getNewContentFor2WeeksByTheme(theme: String, member: Member, pageable: Pageable): GetNewContentAllResponse { - val totalCount = repository.totalCountNewContentFor2Weeks(theme, isAdult = member.auth != null) + val totalCount = + repository.totalCountNewContentFor2Weeks(theme, memberId = member.id!!, isAdult = member.auth != null) val items = repository.findByThemeFor2Weeks( cloudfrontHost = imageHost, + memberId = member.id!!, theme = theme, isAdult = member.auth != null, offset = pageable.offset, @@ -119,31 +122,6 @@ class AudioContentMainService( } .toList() - @Transactional(readOnly = true) - @Cacheable(cacheNames = ["default"], key = "'contentCurationList:' + #memberId + ':' + #isAdult") - fun getAudioContentCurationList(memberId: Long, isAdult: Boolean) = - repository.getAudioContentCurations(isAdult = isAdult) - .asSequence() - .map { - GetAudioContentCurationResponse( - curationId = it.id!!, - title = it.title, - description = it.description, - contents = repository.findAudioContentByCurationId( - curationId = it.id!!, - cloudfrontHost = imageHost, - isAdult = isAdult - ) - .asSequence() - .filter { content -> - !blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = content.creatorId) - } - .toList() - ) - } - .filter { it.contents.isNotEmpty() } - .toList() - @Transactional(readOnly = true) @Cacheable( cacheNames = ["default"], 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 db52c6f..feb941d 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 @@ -161,6 +161,7 @@ class OrderQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Orde .from(order) .where( order.member.id.eq(memberId) + .and(order.isActive.isTrue) .and( order.type.eq(OrderType.KEEP) .or( @@ -199,6 +200,7 @@ class OrderQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Orde .innerJoin(audioContent.member, member) .where( order.member.id.eq(memberId) + .and(order.isActive.isTrue) .and( order.type.eq(OrderType.KEEP) .or( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt index 3acdf58..9d24456 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt @@ -97,7 +97,7 @@ class AuthService( repository.save(auth) } else { - throw SodaException("19세 미만 인증 오류") + throw SodaException("2005년 1월 1일 이전 출생자만 본인인증이 가능합니다.") } } }