test #257
| @@ -0,0 +1,40 @@ | ||||
| package kr.co.vividnext.sodalive.admin.content.series.recommend | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import org.springframework.security.access.prepost.PreAuthorize | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.PostMapping | ||||
| import org.springframework.web.bind.annotation.PutMapping | ||||
| import org.springframework.web.bind.annotation.RequestBody | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RequestParam | ||||
| import org.springframework.web.bind.annotation.RequestPart | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
| import org.springframework.web.multipart.MultipartFile | ||||
|  | ||||
| @RestController | ||||
| @PreAuthorize("hasRole('ADMIN')") | ||||
| @RequestMapping("/admin/audio-content/series/recommend") | ||||
| class AdminRecommendSeriesController(private val service: AdminRecommendSeriesService) { | ||||
|     @GetMapping | ||||
|     fun getRecommendSeriesList(@RequestParam isFree: Boolean) = ApiResponse.ok( | ||||
|         service.getRecommendSeriesList(isFree = isFree) | ||||
|     ) | ||||
|  | ||||
|     @PostMapping | ||||
|     fun createRecommendSeries( | ||||
|         @RequestPart("image") image: MultipartFile, | ||||
|         @RequestPart("request") requestString: String | ||||
|     ) = ApiResponse.ok(service.createRecommendSeries(image, requestString)) | ||||
|  | ||||
|     @PutMapping | ||||
|     fun modifyRecommendSeries( | ||||
|         @RequestPart("image", required = false) image: MultipartFile? = null, | ||||
|         @RequestPart("request") requestString: String | ||||
|     ) = ApiResponse.ok(service.updateRecommendSeries(image, requestString)) | ||||
|  | ||||
|     @PutMapping("/orders") | ||||
|     fun updateRecommendSeriesOrders( | ||||
|         @RequestBody request: UpdateRecommendSeriesOrdersRequest | ||||
|     ) = ApiResponse.ok(service.updateRecommendSeriesOrders(request.ids)) | ||||
| } | ||||
| @@ -0,0 +1,43 @@ | ||||
| package kr.co.vividnext.sodalive.admin.content.series.recommend | ||||
|  | ||||
| import com.querydsl.jpa.impl.JPAQueryFactory | ||||
| import kr.co.vividnext.sodalive.content.main.tab.QRecommendSeries.recommendSeries | ||||
| import kr.co.vividnext.sodalive.content.main.tab.RecommendSeries | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series | ||||
| import org.springframework.beans.factory.annotation.Value | ||||
| import org.springframework.data.jpa.repository.JpaRepository | ||||
|  | ||||
| interface AdminRecommendSeriesRepository : | ||||
|     JpaRepository<RecommendSeries, Long>, | ||||
|     AdminRecommendSeriesQueryRepository | ||||
|  | ||||
| interface AdminRecommendSeriesQueryRepository { | ||||
|     fun getRecommendSeriesList(isFree: Boolean): List<GetAdminRecommendSeriesListResponse> | ||||
| } | ||||
|  | ||||
| class AdminRecommendSeriesQueryRepositoryImpl( | ||||
|     private val queryFactory: JPAQueryFactory, | ||||
|  | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     private val imageHost: String | ||||
| ) : AdminRecommendSeriesQueryRepository { | ||||
|     override fun getRecommendSeriesList(isFree: Boolean): List<GetAdminRecommendSeriesListResponse> { | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QGetAdminRecommendSeriesListResponse( | ||||
|                     recommendSeries.id, | ||||
|                     series.id, | ||||
|                     series.title, | ||||
|                     recommendSeries.imagePath.prepend("/").prepend(imageHost) | ||||
|                 ) | ||||
|             ) | ||||
|             .from(recommendSeries) | ||||
|             .innerJoin(recommendSeries.series, series) | ||||
|             .where( | ||||
|                 recommendSeries.isActive.isTrue | ||||
|                     .and(series.isActive.isTrue) | ||||
|                     .and(recommendSeries.isFree.eq(isFree)) | ||||
|             ) | ||||
|             .fetch() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,87 @@ | ||||
| package kr.co.vividnext.sodalive.admin.content.series.recommend | ||||
|  | ||||
| import com.fasterxml.jackson.databind.ObjectMapper | ||||
| import kr.co.vividnext.sodalive.admin.content.series.AdminContentSeriesRepository | ||||
| import kr.co.vividnext.sodalive.aws.s3.S3Uploader | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.content.main.tab.RecommendSeries | ||||
| import kr.co.vividnext.sodalive.utils.generateFileName | ||||
| import org.springframework.beans.factory.annotation.Value | ||||
| import org.springframework.data.repository.findByIdOrNull | ||||
| import org.springframework.stereotype.Service | ||||
| import org.springframework.transaction.annotation.Transactional | ||||
| import org.springframework.web.multipart.MultipartFile | ||||
|  | ||||
| @Service | ||||
| class AdminRecommendSeriesService( | ||||
|     private val s3Uploader: S3Uploader, | ||||
|     private val repository: AdminRecommendSeriesRepository, | ||||
|     private val seriesRepository: AdminContentSeriesRepository, | ||||
|     private val objectMapper: ObjectMapper, | ||||
|  | ||||
|     @Value("\${cloud.aws.s3.bucket}") | ||||
|     private val bucket: String | ||||
| ) { | ||||
|     fun getRecommendSeriesList(isFree: Boolean): List<GetAdminRecommendSeriesListResponse> { | ||||
|         return repository.getRecommendSeriesList(isFree = isFree) | ||||
|     } | ||||
|  | ||||
|     @Transactional | ||||
|     fun createRecommendSeries(image: MultipartFile, requestString: String) { | ||||
|         val request = objectMapper.readValue(requestString, CreateRecommendSeriesRequest::class.java) | ||||
|         val series = seriesRepository.findByIdOrNull(request.seriesId) | ||||
|             ?: throw SodaException("잘못된 요청입니다.") | ||||
|  | ||||
|         val recommendSeries = RecommendSeries(isFree = request.isFree) | ||||
|         recommendSeries.series = series | ||||
|         repository.save(recommendSeries) | ||||
|  | ||||
|         val fileName = generateFileName() | ||||
|         val imagePath = s3Uploader.upload( | ||||
|             inputStream = image.inputStream, | ||||
|             bucket = bucket, | ||||
|             filePath = "recommend_series/${recommendSeries.id}/$fileName" | ||||
|         ) | ||||
|         recommendSeries.imagePath = imagePath | ||||
|     } | ||||
|  | ||||
|     @Transactional | ||||
|     fun updateRecommendSeries(image: MultipartFile?, requestString: String) { | ||||
|         val request = objectMapper.readValue(requestString, UpdateRecommendSeriesRequest::class.java) | ||||
|         val recommendSeries = repository.findByIdOrNull(request.id) | ||||
|             ?: throw SodaException("잘못된 요청입니다.") | ||||
|  | ||||
|         if (image != null) { | ||||
|             val fileName = generateFileName() | ||||
|             val imagePath = s3Uploader.upload( | ||||
|                 inputStream = image.inputStream, | ||||
|                 bucket = bucket, | ||||
|                 filePath = "recommend_series/${recommendSeries.id}/$fileName" | ||||
|             ) | ||||
|             recommendSeries.imagePath = imagePath | ||||
|         } | ||||
|  | ||||
|         if (request.isActive != null) { | ||||
|             recommendSeries.isActive = request.isActive | ||||
|         } | ||||
|  | ||||
|         if (request.seriesId != null) { | ||||
|             val series = seriesRepository.findByIdOrNull(request.seriesId) | ||||
|  | ||||
|             if (series != null) { | ||||
|                 recommendSeries.series = series | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Transactional | ||||
|     fun updateRecommendSeriesOrders(ids: List<Long>) { | ||||
|         for (index in ids.indices) { | ||||
|             val recommendSeries = repository.findByIdOrNull(ids[index]) | ||||
|  | ||||
|             if (recommendSeries != null) { | ||||
|                 recommendSeries.orders = index + 1 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,6 @@ | ||||
| package kr.co.vividnext.sodalive.admin.content.series.recommend | ||||
|  | ||||
| data class CreateRecommendSeriesRequest( | ||||
|     val seriesId: Long, | ||||
|     val isFree: Boolean | ||||
| ) | ||||
| @@ -0,0 +1,10 @@ | ||||
| package kr.co.vividnext.sodalive.admin.content.series.recommend | ||||
|  | ||||
| import com.querydsl.core.annotations.QueryProjection | ||||
|  | ||||
| data class GetAdminRecommendSeriesListResponse @QueryProjection constructor( | ||||
|     val id: Long, | ||||
|     val seriesId: Long, | ||||
|     val seriesTitle: String, | ||||
|     val imageUrl: String | ||||
| ) | ||||
| @@ -0,0 +1,5 @@ | ||||
| package kr.co.vividnext.sodalive.admin.content.series.recommend | ||||
|  | ||||
| data class UpdateRecommendSeriesOrdersRequest( | ||||
|     val ids: List<Long> | ||||
| ) | ||||
| @@ -0,0 +1,7 @@ | ||||
| package kr.co.vividnext.sodalive.admin.content.series.recommend | ||||
|  | ||||
| data class UpdateRecommendSeriesRequest( | ||||
|     val id: Long, | ||||
|     val seriesId: Long?, | ||||
|     val isActive: Boolean? | ||||
| ) | ||||
| @@ -42,8 +42,8 @@ class AuditionVoteService( | ||||
|             endDate = endDate | ||||
|         ) | ||||
|  | ||||
|         if (voteCount > 10) { | ||||
|             throw SodaException("오늘 응원은 여기까지!\n하루 최대 10회까지 응원이 가능합니다.\n내일 다시 이용해주세요.") | ||||
|         if (voteCount > 100) { | ||||
|             throw SodaException("오늘 응원은 여기까지!\n하루 최대 100회까지 응원이 가능합니다.\n내일 다시 이용해주세요.") | ||||
|         } | ||||
|  | ||||
|         if (voteCount > 0) { | ||||
|   | ||||
| @@ -6,9 +6,9 @@ import kr.co.vividnext.sodalive.content.QAudioContent.audioContent | ||||
| import kr.co.vividnext.sodalive.content.category.QCategoryContent.categoryContent | ||||
| import kr.co.vividnext.sodalive.content.comment.QAudioContentComment.audioContentComment | ||||
| import kr.co.vividnext.sodalive.content.like.QAudioContentLike.audioContentLike | ||||
| import kr.co.vividnext.sodalive.content.main.ContentCreatorResponse | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem | ||||
| import kr.co.vividnext.sodalive.content.main.GetNewContentUploadCreator | ||||
| import kr.co.vividnext.sodalive.content.main.QGetAudioContentMainItem | ||||
| import kr.co.vividnext.sodalive.content.main.QGetAudioContentRankingItem | ||||
| import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner | ||||
| @@ -24,6 +24,7 @@ import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentThe | ||||
| import kr.co.vividnext.sodalive.event.QEvent.event | ||||
| import kr.co.vividnext.sodalive.member.MemberRole | ||||
| import kr.co.vividnext.sodalive.member.QMember.member | ||||
| import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember | ||||
| import org.springframework.beans.factory.annotation.Value | ||||
| import org.springframework.data.jpa.repository.JpaRepository | ||||
| import org.springframework.stereotype.Repository | ||||
| @@ -61,14 +62,14 @@ interface AudioContentQueryRepository { | ||||
|     ): List<OtherContentResponse> | ||||
|  | ||||
|     fun findByTheme( | ||||
|         cloudfrontHost: String, | ||||
|         memberId: Long, | ||||
|         theme: String = "", | ||||
|         sortType: SortType = SortType.NEWEST, | ||||
|         isAdult: Boolean = false, | ||||
|         contentType: ContentType, | ||||
|         offset: Long = 0, | ||||
|         limit: Long = 20 | ||||
|         limit: Long = 20, | ||||
|         isFree: Boolean = false | ||||
|     ): List<GetAudioContentMainItem> | ||||
|  | ||||
|     fun totalCountByTheme( | ||||
| @@ -98,13 +99,12 @@ interface AudioContentQueryRepository { | ||||
|     fun getNewContentUploadCreatorList( | ||||
|         cloudfrontHost: String, | ||||
|         isAdult: Boolean = false | ||||
|     ): List<GetNewContentUploadCreator> | ||||
|     ): List<ContentCreatorResponse> | ||||
|  | ||||
|     fun getAudioContentMainBannerList(isAdult: Boolean): List<AudioContentBanner> | ||||
|     fun getAudioContentCurations(isAdult: Boolean): List<AudioContentCuration> | ||||
|     fun findAudioContentByCurationId( | ||||
|         curationId: Long, | ||||
|         cloudfrontHost: String, | ||||
|         isAdult: Boolean, | ||||
|         contentType: ContentType | ||||
|     ): List<GetAudioContentMainItem> | ||||
| @@ -334,15 +334,19 @@ class AudioContentQueryRepositoryImpl( | ||||
|     } | ||||
|  | ||||
|     override fun findByTheme( | ||||
|         cloudfrontHost: String, | ||||
|         memberId: Long, | ||||
|         theme: String, | ||||
|         sortType: SortType, | ||||
|         isAdult: Boolean, | ||||
|         contentType: ContentType, | ||||
|         offset: Long, | ||||
|         limit: Long | ||||
|         limit: Long, | ||||
|         isFree: Boolean | ||||
|     ): List<GetAudioContentMainItem> { | ||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) | ||||
|             .and(blockMember.isActive.isTrue) | ||||
|             .and(blockMember.blockedMember.id.eq(memberId)) | ||||
|  | ||||
|         val orderBy = when (sortType) { | ||||
|             SortType.NEWEST -> listOf(audioContent.releaseDate.desc(), audioContent.id.desc()) | ||||
|             SortType.PRICE_HIGH -> listOf( | ||||
| @@ -365,6 +369,7 @@ class AudioContentQueryRepositoryImpl( | ||||
|                     .or(audioContent.releaseDate.loe(LocalDateTime.now())) | ||||
|                     .or(audioContent.member.id.eq(memberId)) | ||||
|             ) | ||||
|             .and(blockMember.id.isNull) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(audioContent.isAdult.isFalse) | ||||
| @@ -380,14 +385,18 @@ class AudioContentQueryRepositoryImpl( | ||||
|             where = where.and(audioContentTheme.theme.eq(theme)) | ||||
|         } | ||||
|  | ||||
|         if (isFree) { | ||||
|             where = where.and(audioContent.price.loe(0)) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QGetAudioContentMainItem( | ||||
|                     audioContent.id, | ||||
|                     audioContent.coverImage.prepend("/").prepend(cloudfrontHost), | ||||
|                     audioContent.coverImage.prepend("/").prepend(imageHost), | ||||
|                     audioContent.title, | ||||
|                     member.id, | ||||
|                     member.profileImage.prepend("/").prepend(cloudfrontHost), | ||||
|                     member.profileImage.prepend("/").prepend(imageHost), | ||||
|                     member.nickname, | ||||
|                     audioContent.price, | ||||
|                     audioContent.duration | ||||
| @@ -396,6 +405,7 @@ class AudioContentQueryRepositoryImpl( | ||||
|             .from(audioContent) | ||||
|             .innerJoin(audioContent.member, member) | ||||
|             .innerJoin(audioContent.theme, audioContentTheme) | ||||
|             .leftJoin(blockMember).on(blockMemberCondition) | ||||
|             .where(where) | ||||
|             .offset(offset) | ||||
|             .limit(limit) | ||||
| @@ -533,7 +543,7 @@ class AudioContentQueryRepositoryImpl( | ||||
|     override fun getNewContentUploadCreatorList( | ||||
|         cloudfrontHost: String, | ||||
|         isAdult: Boolean | ||||
|     ): List<GetNewContentUploadCreator> { | ||||
|     ): List<ContentCreatorResponse> { | ||||
|         var where = audioContent.releaseDate.after(LocalDateTime.now().minusWeeks(2)) | ||||
|             .and(audioContent.isActive.isTrue) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
| @@ -552,7 +562,7 @@ class AudioContentQueryRepositoryImpl( | ||||
|             .limit(20) | ||||
|             .fetch() | ||||
|             .map { | ||||
|                 GetNewContentUploadCreator( | ||||
|                 ContentCreatorResponse( | ||||
|                     it.id!!, | ||||
|                     it.nickname, | ||||
|                     creatorProfileImageUrl = if (it.profileImage != null) { | ||||
| @@ -596,7 +606,6 @@ class AudioContentQueryRepositoryImpl( | ||||
|  | ||||
|     override fun findAudioContentByCurationId( | ||||
|         curationId: Long, | ||||
|         cloudfrontHost: String, | ||||
|         isAdult: Boolean, | ||||
|         contentType: ContentType | ||||
|     ): List<GetAudioContentMainItem> { | ||||
| @@ -620,12 +629,12 @@ class AudioContentQueryRepositoryImpl( | ||||
|             .select( | ||||
|                 QGetAudioContentMainItem( | ||||
|                     audioContent.id, | ||||
|                     audioContent.coverImage.prepend("/").prepend(cloudfrontHost), | ||||
|                     audioContent.coverImage.prepend("/").prepend(imageHost), | ||||
|                     audioContent.title, | ||||
|                     member.id, | ||||
|                     member.profileImage | ||||
|                         .prepend("/") | ||||
|                         .prepend(cloudfrontHost), | ||||
|                         .prepend(imageHost), | ||||
|                     member.nickname, | ||||
|                     audioContent.price, | ||||
|                     audioContent.duration | ||||
|   | ||||
| @@ -39,7 +39,6 @@ class AudioContentMainService( | ||||
|         pageable: Pageable | ||||
|     ): List<GetAudioContentMainItem> { | ||||
|         return repository.findByTheme( | ||||
|             cloudfrontHost = imageHost, | ||||
|             memberId = member.id!!, | ||||
|             theme = theme, | ||||
|             isAdult = member.auth != null && isAdultContentVisible, | ||||
| @@ -47,7 +46,6 @@ class AudioContentMainService( | ||||
|             offset = pageable.offset, | ||||
|             limit = pageable.pageSize.toLong() | ||||
|         ) | ||||
|             .filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creatorId) } | ||||
|     } | ||||
|  | ||||
|     @Transactional(readOnly = true) | ||||
| @@ -80,7 +78,7 @@ class AudioContentMainService( | ||||
|  | ||||
|     @Transactional(readOnly = true) | ||||
|     @Cacheable(cacheNames = ["default"], key = "'newContentUploadCreatorList:' + #memberId + ':' + #isAdult") | ||||
|     fun getNewContentUploadCreatorList(memberId: Long, isAdult: Boolean): List<GetNewContentUploadCreator> { | ||||
|     fun getNewContentUploadCreatorList(memberId: Long, isAdult: Boolean): List<ContentCreatorResponse> { | ||||
|         return repository.getNewContentUploadCreatorList( | ||||
|             cloudfrontHost = imageHost, | ||||
|             isAdult = isAdult | ||||
| @@ -164,7 +162,6 @@ class AudioContentMainService( | ||||
|                 description = it.description, | ||||
|                 contents = repository.findAudioContentByCurationId( | ||||
|                     curationId = it.id!!, | ||||
|                     cloudfrontHost = imageHost, | ||||
|                     isAdult = isAdult, | ||||
|                     contentType = contentType | ||||
|                 ) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ package kr.co.vividnext.sodalive.content.main | ||||
| import com.fasterxml.jackson.annotation.JsonProperty | ||||
| import com.querydsl.core.annotations.QueryProjection | ||||
| 
 | ||||
| data class GetNewContentUploadCreator @QueryProjection constructor( | ||||
| data class ContentCreatorResponse @QueryProjection constructor( | ||||
|     @JsonProperty("creatorId") val creatorId: Long, | ||||
|     @JsonProperty("creatorNickname") val creatorNickname: String, | ||||
|     @JsonProperty("creatorProfileImageUrl") val creatorProfileImageUrl: String | ||||
| @@ -0,0 +1,41 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.banner | ||||
|  | ||||
| import com.querydsl.jpa.impl.JPAQueryFactory | ||||
| import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioContentBanner | ||||
| import kr.co.vividnext.sodalive.content.main.tab.QAudioContentMainTab.audioContentMainTab | ||||
| import kr.co.vividnext.sodalive.event.QEvent.event | ||||
| import kr.co.vividnext.sodalive.member.QMember.member | ||||
| import org.springframework.data.jpa.repository.JpaRepository | ||||
|  | ||||
| interface AudioContentBannerRepository : JpaRepository<AudioContentBanner, Long>, AudioContentBannerQueryRepository | ||||
|  | ||||
| interface AudioContentBannerQueryRepository { | ||||
|     fun getAudioContentMainBannerList(tabId: Long, isAdult: Boolean): List<AudioContentBanner> | ||||
| } | ||||
|  | ||||
| class AudioContentBannerQueryRepositoryImpl( | ||||
|     private val queryFactory: JPAQueryFactory | ||||
| ) : AudioContentBannerQueryRepository { | ||||
|     override fun getAudioContentMainBannerList(tabId: Long, isAdult: Boolean): List<AudioContentBanner> { | ||||
|         var where = audioContentBanner.isActive.isTrue | ||||
|  | ||||
|         where = if (tabId == 1L) { | ||||
|             where.and(audioContentBanner.tab.isNull) | ||||
|         } else { | ||||
|             where.and(audioContentBanner.tab.id.eq(tabId)) | ||||
|         } | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(audioContentBanner.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .selectFrom(audioContentBanner) | ||||
|             .leftJoin(audioContentBanner.tab, audioContentMainTab) | ||||
|             .leftJoin(audioContentBanner.event, event) | ||||
|             .leftJoin(audioContentBanner.creator, member) | ||||
|             .where(where) | ||||
|             .orderBy(audioContentBanner.orders.asc()) | ||||
|             .fetch() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,67 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.banner | ||||
|  | ||||
| import kr.co.vividnext.sodalive.event.EventItem | ||||
| import kr.co.vividnext.sodalive.member.block.BlockMemberRepository | ||||
| import org.springframework.beans.factory.annotation.Value | ||||
| import org.springframework.stereotype.Service | ||||
|  | ||||
| @Service | ||||
| class AudioContentBannerService( | ||||
|     private val repository: AudioContentBannerRepository, | ||||
|     private val blockMemberRepository: BlockMemberRepository, | ||||
|  | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     private val imageHost: String | ||||
| ) { | ||||
|     fun getBannerList(tabId: Long, memberId: Long, isAdult: Boolean): List<GetAudioContentBannerResponse> { | ||||
|         return repository.getAudioContentMainBannerList(tabId, isAdult) | ||||
|             .filter { | ||||
|                 if (it.type == AudioContentBannerType.CREATOR && it.creator != null) { | ||||
|                     !blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.creator!!.id!!) | ||||
|                 } else if (it.type == AudioContentBannerType.SERIES && it.series != null) { | ||||
|                     !blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.series!!.member!!.id!!) | ||||
|                 } else { | ||||
|                     true | ||||
|                 } | ||||
|             } | ||||
|             .map { | ||||
|                 GetAudioContentBannerResponse( | ||||
|                     type = it.type, | ||||
|                     thumbnailImageUrl = "$imageHost/${it.thumbnailImage}", | ||||
|                     eventItem = if (it.type == AudioContentBannerType.EVENT && it.event != null) { | ||||
|                         EventItem( | ||||
|                             id = it.event!!.id!!, | ||||
|                             thumbnailImageUrl = if (!it.event!!.thumbnailImage.startsWith("https://")) { | ||||
|                                 "$imageHost/${it.event!!.thumbnailImage}" | ||||
|                             } else { | ||||
|                                 it.event!!.thumbnailImage | ||||
|                             }, | ||||
|                             detailImageUrl = if ( | ||||
|                                 it.event!!.detailImage != null && | ||||
|                                 !it.event!!.detailImage!!.startsWith("https://") | ||||
|                             ) { | ||||
|                                 "$imageHost/${it.event!!.detailImage}" | ||||
|                             } else { | ||||
|                                 it.event!!.detailImage | ||||
|                             }, | ||||
|                             popupImageUrl = null, | ||||
|                             link = it.event!!.link | ||||
|                         ) | ||||
|                     } else { | ||||
|                         null | ||||
|                     }, | ||||
|                     creatorId = if (it.type == AudioContentBannerType.CREATOR && it.creator != null) { | ||||
|                         it.creator!!.id | ||||
|                     } else { | ||||
|                         null | ||||
|                     }, | ||||
|                     seriesId = if (it.type == AudioContentBannerType.SERIES && it.series != null) { | ||||
|                         it.series!!.id | ||||
|                     } else { | ||||
|                         null | ||||
|                     }, | ||||
|                     link = it.link | ||||
|                 ) | ||||
|             } | ||||
|     } | ||||
| } | ||||
| @@ -6,6 +6,8 @@ import kr.co.vividnext.sodalive.content.QAudioContent.audioContent | ||||
| import kr.co.vividnext.sodalive.content.SortType | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem | ||||
| import kr.co.vividnext.sodalive.content.main.QGetAudioContentMainItem | ||||
| import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration | ||||
| import kr.co.vividnext.sodalive.content.main.tab.QAudioContentMainTab.audioContentMainTab | ||||
| import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme | ||||
| import kr.co.vividnext.sodalive.member.QMember.member | ||||
| import org.springframework.stereotype.Repository | ||||
| @@ -87,4 +89,42 @@ class AudioContentCurationQueryRepository(private val queryFactory: JPAQueryFact | ||||
|             .orderBy(orderBy) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun findByContentMainTabId(tabId: Long, isAdult: Boolean): List<AudioContentCuration> { | ||||
|         var where = audioContentCuration.isActive.isTrue | ||||
|             .and(audioContentMainTab.id.eq(tabId)) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(audioContentCuration.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .selectFrom(audioContentCuration) | ||||
|             .innerJoin(audioContentCuration.tab, audioContentMainTab) | ||||
|             .where(where) | ||||
|             .orderBy(audioContentCuration.orders.asc()) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun findByContentMainTabIdAndTitle( | ||||
|         tabId: Long, | ||||
|         title: String, | ||||
|         isAdult: Boolean, | ||||
|         offset: Long = 0, | ||||
|         limit: Long = 12 | ||||
|     ): List<AudioContentCuration> { | ||||
|         var where = audioContentCuration.isActive.isTrue | ||||
|             .and(audioContentMainTab.id.eq(tabId)) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(audioContentCuration.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .selectFrom(audioContentCuration) | ||||
|             .innerJoin(audioContentCuration.tab, audioContentMainTab) | ||||
|             .where(where) | ||||
|             .orderBy(audioContentCuration.orders.asc()) | ||||
|             .fetch() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem | ||||
|  | ||||
| data class GetContentCurationResponse( | ||||
|     val title: String, | ||||
|     val items: List<GetAudioContentMainItem> | ||||
| ) | ||||
| @@ -0,0 +1,11 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab | ||||
|  | ||||
| import com.querydsl.core.annotations.QueryProjection | ||||
|  | ||||
| data class GetRecommendSeriesListResponse @QueryProjection constructor( | ||||
|     val seriesId: Long, | ||||
|     val title: String, | ||||
|     val imageUrl: String, | ||||
|     val creatorId: Long, | ||||
|     val creatorNickname: String | ||||
| ) | ||||
| @@ -0,0 +1,28 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.BaseEntity | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.Series | ||||
| import javax.persistence.Column | ||||
| import javax.persistence.Entity | ||||
| import javax.persistence.FetchType | ||||
| import javax.persistence.JoinColumn | ||||
| import javax.persistence.ManyToOne | ||||
|  | ||||
| @Entity | ||||
| data class RecommendSeries( | ||||
|     @Column(nullable = false) | ||||
|     var imagePath: String = "", | ||||
|  | ||||
|     @Column(nullable = false) | ||||
|     var orders: Int = 1, | ||||
|  | ||||
|     @Column(nullable = false) | ||||
|     var isFree: Boolean = false, | ||||
|  | ||||
|     @Column(nullable = false) | ||||
|     var isActive: Boolean = true | ||||
| ) : BaseEntity() { | ||||
|     @ManyToOne(fetch = FetchType.LAZY) | ||||
|     @JoinColumn(name = "series_id", nullable = false) | ||||
|     var series: Series? = null | ||||
| } | ||||
| @@ -0,0 +1,70 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab | ||||
|  | ||||
| import com.querydsl.jpa.impl.JPAQueryFactory | ||||
| import kr.co.vividnext.sodalive.content.main.tab.QRecommendSeries.recommendSeries | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series | ||||
| import kr.co.vividnext.sodalive.member.QMember.member | ||||
| import org.springframework.beans.factory.annotation.Value | ||||
| import org.springframework.stereotype.Repository | ||||
|  | ||||
| @Repository | ||||
| class RecommendSeriesRepository( | ||||
|     private val queryFactory: JPAQueryFactory, | ||||
|  | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     private val imageHost: String | ||||
| ) { | ||||
|     fun getNewSeriesList(isAdult: Boolean): List<GetRecommendSeriesListResponse> { | ||||
|         var where = recommendSeries.isActive.isTrue | ||||
|             .and(recommendSeries.isFree.isFalse) | ||||
|             .and(series.isActive.isTrue) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QGetRecommendSeriesListResponse( | ||||
|                     series.id, | ||||
|                     series.title, | ||||
|                     recommendSeries.imagePath.prepend("/").prepend(imageHost), | ||||
|                     member.id, | ||||
|                     member.nickname | ||||
|                 ) | ||||
|             ) | ||||
|             .from(recommendSeries) | ||||
|             .innerJoin(recommendSeries.series, series) | ||||
|             .innerJoin(series.member, member) | ||||
|             .where(where) | ||||
|             .orderBy(recommendSeries.orders.asc()) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun getRecommendSeriesList(isAdult: Boolean): List<GetRecommendSeriesListResponse> { | ||||
|         var where = recommendSeries.isActive.isTrue | ||||
|             .and(recommendSeries.isFree.isTrue) | ||||
|             .and(series.isActive.isTrue) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QGetRecommendSeriesListResponse( | ||||
|                     series.id, | ||||
|                     series.title, | ||||
|                     recommendSeries.imagePath.prepend("/").prepend(imageHost), | ||||
|                     member.id, | ||||
|                     member.nickname | ||||
|                 ) | ||||
|             ) | ||||
|             .from(recommendSeries) | ||||
|             .innerJoin(recommendSeries.series, series) | ||||
|             .innerJoin(series.member, member) | ||||
|             .where(where) | ||||
|             .orderBy(recommendSeries.orders.asc()) | ||||
|             .fetch() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,22 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.alarm | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/v2/audio-content/main/alarm") | ||||
| class AudioContentMainTabAlarmController(private val service: AudioContentMainTabAlarmService) { | ||||
|     @GetMapping | ||||
|     fun fetchContentMainTabAlarm( | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok(service.fetchData(member)) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,84 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.alarm | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.AudioContentRepository | ||||
| import kr.co.vividnext.sodalive.content.ContentType | ||||
| import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService | ||||
| import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryRepository | ||||
| import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse | ||||
| import kr.co.vividnext.sodalive.event.EventService | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import kr.co.vividnext.sodalive.rank.RankingService | ||||
| import org.springframework.stereotype.Service | ||||
| import java.time.DayOfWeek | ||||
| import java.time.LocalDateTime | ||||
| import java.time.temporal.TemporalAdjusters | ||||
|  | ||||
| @Service | ||||
| class AudioContentMainTabAlarmService( | ||||
|     private val bannerService: AudioContentBannerService, | ||||
|     private val contentRepository: AudioContentRepository, | ||||
|     private val rankingService: RankingService, | ||||
|     private val eventService: EventService, | ||||
|     private val curationRepository: AudioContentCurationQueryRepository | ||||
| ) { | ||||
|     fun fetchData(member: Member): GetContentMainTabAlarmResponse { | ||||
|         val isAdult = member.auth != null | ||||
|         val memberId = member.id!! | ||||
|  | ||||
|         val contentBannerList = bannerService.getBannerList( | ||||
|             tabId = 4, | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult | ||||
|         ) | ||||
|  | ||||
|         val alarmThemeList = listOf("모닝콜", "슬립콜", "알람") | ||||
|         val newAlarmContentList = contentRepository.findByTheme( | ||||
|             memberId = memberId, | ||||
|             theme = alarmThemeList[0], | ||||
|             isAdult = isAdult, | ||||
|             contentType = ContentType.ALL, | ||||
|             limit = 10 | ||||
|         ) | ||||
|  | ||||
|         // 주간 랭킹 기간 | ||||
|         val currentDateTime = LocalDateTime.now() | ||||
|         val startDate = currentDateTime | ||||
|             .withHour(15) | ||||
|             .withMinute(0) | ||||
|             .withSecond(0) | ||||
|             .minusWeeks(1) | ||||
|             .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) | ||||
|         val endDate = startDate | ||||
|             .plusDays(6) | ||||
|  | ||||
|         val rankAlarmContentList = rankingService.getContentRanking( | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult, | ||||
|             startDate = startDate, | ||||
|             endDate = endDate, | ||||
|             theme = alarmThemeList[0] | ||||
|         ) | ||||
|  | ||||
|         val eventBannerList = eventService.getEventList(isAdult = isAdult) | ||||
|         val curationList = curationRepository.findByContentMainTabId(tabId = 4, isAdult = isAdult) | ||||
|             .map { | ||||
|                 GetContentCurationResponse( | ||||
|                     title = it.title, | ||||
|                     items = contentRepository.findAudioContentByCurationId( | ||||
|                         curationId = it.id!!, | ||||
|                         isAdult = isAdult, | ||||
|                         contentType = ContentType.ALL | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|         return GetContentMainTabAlarmResponse( | ||||
|             contentBannerList = contentBannerList, | ||||
|             alarmThemeList = alarmThemeList, | ||||
|             newAlarmContentList = newAlarmContentList, | ||||
|             rankAlarmContentList = rankAlarmContentList, | ||||
|             eventBannerList = eventBannerList, | ||||
|             curationList = curationList | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,17 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.alarm | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem | ||||
| import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse | ||||
| import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse | ||||
| import kr.co.vividnext.sodalive.event.GetEventResponse | ||||
|  | ||||
| data class GetContentMainTabAlarmResponse( | ||||
|     val tab: Long = 4, | ||||
|     val contentBannerList: List<GetAudioContentBannerResponse>, | ||||
|     val alarmThemeList: List<String>, | ||||
|     val newAlarmContentList: List<GetAudioContentMainItem>, | ||||
|     val rankAlarmContentList: List<GetAudioContentRankingItem>, | ||||
|     val eventBannerList: GetEventResponse, | ||||
|     val curationList: List<GetContentCurationResponse> | ||||
| ) | ||||
| @@ -0,0 +1,22 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.asmr | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/v2/audio-content/main/asmr") | ||||
| class AudioContentMainTabAsmrController(private val service: AudioContentMainTabAsmrService) { | ||||
|     @GetMapping | ||||
|     fun fetchContentMainTabAsmr( | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok(service.fetchData(member)) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,84 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.asmr | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.AudioContentRepository | ||||
| import kr.co.vividnext.sodalive.content.ContentType | ||||
| import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService | ||||
| import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryRepository | ||||
| import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse | ||||
| import kr.co.vividnext.sodalive.event.EventService | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import kr.co.vividnext.sodalive.rank.RankingService | ||||
| import org.springframework.stereotype.Service | ||||
| import java.time.DayOfWeek | ||||
| import java.time.LocalDateTime | ||||
| import java.time.temporal.TemporalAdjusters | ||||
|  | ||||
| @Service | ||||
| class AudioContentMainTabAsmrService( | ||||
|     private val bannerService: AudioContentBannerService, | ||||
|     private val contentRepository: AudioContentRepository, | ||||
|     private val rankingService: RankingService, | ||||
|     private val eventService: EventService, | ||||
|     private val curationRepository: AudioContentCurationQueryRepository | ||||
| ) { | ||||
|     fun fetchData(member: Member): GetContentMainTabAsmrResponse { | ||||
|         val isAdult = member.auth != null | ||||
|         val memberId = member.id!! | ||||
|         val theme = "ASMR" | ||||
|         val tabId = 5L | ||||
|  | ||||
|         val contentBannerList = bannerService.getBannerList( | ||||
|             tabId = tabId, | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult | ||||
|         ) | ||||
|  | ||||
|         val newAsmrContentList = contentRepository.findByTheme( | ||||
|             memberId = memberId, | ||||
|             theme = theme, | ||||
|             isAdult = isAdult, | ||||
|             contentType = ContentType.ALL, | ||||
|             limit = 10 | ||||
|         ) | ||||
|  | ||||
|         val currentDateTime = LocalDateTime.now() | ||||
|         val startDate = currentDateTime | ||||
|             .withHour(15) | ||||
|             .withMinute(0) | ||||
|             .withSecond(0) | ||||
|             .minusWeeks(1) | ||||
|             .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) | ||||
|         val endDate = startDate | ||||
|             .plusDays(6) | ||||
|  | ||||
|         val rankAsmrContentList = rankingService.getContentRanking( | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult, | ||||
|             startDate = startDate, | ||||
|             endDate = endDate, | ||||
|             theme = theme | ||||
|         ) | ||||
|  | ||||
|         val eventBannerList = eventService.getEventList(isAdult = isAdult) | ||||
|  | ||||
|         val curationList = curationRepository.findByContentMainTabId(tabId = tabId, isAdult = isAdult) | ||||
|             .map { | ||||
|                 GetContentCurationResponse( | ||||
|                     title = it.title, | ||||
|                     items = contentRepository.findAudioContentByCurationId( | ||||
|                         curationId = it.id!!, | ||||
|                         isAdult = isAdult, | ||||
|                         contentType = ContentType.ALL | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|         return GetContentMainTabAsmrResponse( | ||||
|             contentBannerList = contentBannerList, | ||||
|             newAsmrContentList = newAsmrContentList, | ||||
|             rankAsmrContentList = rankAsmrContentList, | ||||
|             eventBannerList = eventBannerList, | ||||
|             curationList = curationList | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.asmr | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem | ||||
| import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse | ||||
| import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse | ||||
| import kr.co.vividnext.sodalive.event.GetEventResponse | ||||
|  | ||||
| data class GetContentMainTabAsmrResponse( | ||||
|     val tab: Long = 5, | ||||
|     val contentBannerList: List<GetAudioContentBannerResponse>, | ||||
|     val newAsmrContentList: List<GetAudioContentMainItem>, | ||||
|     val rankAsmrContentList: List<GetAudioContentRankingItem>, | ||||
|     val eventBannerList: GetEventResponse, | ||||
|     val curationList: List<GetContentCurationResponse> | ||||
| ) | ||||
| @@ -0,0 +1,22 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.content | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/v2/audio-content/main/content") | ||||
| class AudioContentMainTabContentController(private val service: AudioContentMainTabContentService) { | ||||
|     @GetMapping | ||||
|     fun fetchContentMainTabContent( | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok(service.fetchData(member)) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,105 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.content | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.AudioContentRepository | ||||
| import kr.co.vividnext.sodalive.content.ContentType | ||||
| import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService | ||||
| import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository | ||||
| import kr.co.vividnext.sodalive.event.EventService | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import kr.co.vividnext.sodalive.rank.RankingService | ||||
| import org.springframework.stereotype.Service | ||||
| import java.time.LocalDateTime | ||||
|  | ||||
| @Service | ||||
| class AudioContentMainTabContentService( | ||||
|     private val bannerService: AudioContentBannerService, | ||||
|     private val audioContentRepository: AudioContentRepository, | ||||
|     private val audioContentThemeRepository: AudioContentThemeQueryRepository, | ||||
|     private val rankingService: RankingService, | ||||
|     private val eventService: EventService | ||||
| ) { | ||||
|     fun fetchData(member: Member): GetContentMainTabContentResponse { | ||||
|         val memberId = member.id!! | ||||
|         val isAdult = member.auth != null | ||||
|  | ||||
|         // 단편 배너 | ||||
|         val contentBannerList = bannerService.getBannerList( | ||||
|             tabId = 3, | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult | ||||
|         ) | ||||
|  | ||||
|         // 새로운 단편 테마 | ||||
|         val themeOfContentList = audioContentThemeRepository.getActiveThemeOfContent(isAdult = isAdult) | ||||
|  | ||||
|         // 새로운 단편 | ||||
|         val newContentList = if (themeOfContentList.isNotEmpty()) { | ||||
|             audioContentRepository.findByTheme( | ||||
|                 memberId = member.id!!, | ||||
|                 theme = themeOfContentList[0], | ||||
|                 isAdult = member.auth != null, | ||||
|                 contentType = ContentType.ALL, | ||||
|                 offset = 0, | ||||
|                 limit = 10 | ||||
|             ) | ||||
|         } else { | ||||
|             emptyList() | ||||
|         } | ||||
|  | ||||
|         // 일간 랭킹 | ||||
|         val currentDateTime = LocalDateTime.now() | ||||
|         val dailyRankingStartDate = currentDateTime | ||||
|             .withHour(15) | ||||
|             .withMinute(0) | ||||
|             .withSecond(0) | ||||
|             .minusDays(2) | ||||
|         val dailyRankingEndDate = dailyRankingStartDate | ||||
|             .plusDays(1) | ||||
|  | ||||
|         val rankContentList = rankingService.getContentRanking( | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult, | ||||
|             startDate = dailyRankingStartDate, | ||||
|             endDate = dailyRankingEndDate | ||||
|         ) | ||||
|  | ||||
|         // 이벤트 배너 | ||||
|         val eventBannerList = eventService.getEventList(isAdult = isAdult) | ||||
|  | ||||
|         val contentRankCreatorList = rankingService.fetchCreatorByContentRevenueRankTop20( | ||||
|             memberId = member.id!!, | ||||
|             startDate = dailyRankingStartDate.minusDays(1), | ||||
|             endDate = dailyRankingEndDate | ||||
|         ) | ||||
|  | ||||
|         val salesRankContentList = if (contentRankCreatorList.isNotEmpty()) { | ||||
|             rankingService.fetchCreatorContentBySalesTop2( | ||||
|                 creatorId = contentRankCreatorList[0].creatorId, | ||||
|                 isAdult = member.auth != null | ||||
|             ) | ||||
|         } else { | ||||
|             emptyList() | ||||
|         } | ||||
|  | ||||
|         val salesCountRankContentList = if (contentRankCreatorList.isNotEmpty()) { | ||||
|             rankingService.fetchCreatorContentBySalesCountTop2( | ||||
|                 creatorId = contentRankCreatorList[0].creatorId, | ||||
|                 isAdult = member.auth != null | ||||
|             ) | ||||
|         } else { | ||||
|             emptyList() | ||||
|         } | ||||
|  | ||||
|         return GetContentMainTabContentResponse( | ||||
|             bannerList = contentBannerList, | ||||
|             contentThemeList = themeOfContentList, | ||||
|             newContentList = newContentList, | ||||
|             rankSortTypeList = listOf("매출", "댓글", "좋아요"), | ||||
|             rankContentList = rankContentList, | ||||
|             contentRankCreatorList = contentRankCreatorList, | ||||
|             salesRankContentList = salesRankContentList, | ||||
|             salesCountRankContentList = salesCountRankContentList, | ||||
|             eventBannerList = eventBannerList | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.content | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.ContentCreatorResponse | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem | ||||
| import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse | ||||
| import kr.co.vividnext.sodalive.event.GetEventResponse | ||||
|  | ||||
| data class GetContentMainTabContentResponse( | ||||
|     val tabId: Long = 3, | ||||
|     val bannerList: List<GetAudioContentBannerResponse>, | ||||
|     val contentThemeList: List<String>, | ||||
|     val newContentList: List<GetAudioContentMainItem>, | ||||
|     val rankSortTypeList: List<String>, | ||||
|     val rankContentList: List<GetAudioContentRankingItem>, | ||||
|     val contentRankCreatorList: List<ContentCreatorResponse>, | ||||
|     val salesRankContentList: List<GetAudioContentRankingItem>, | ||||
|     val salesCountRankContentList: List<GetAudioContentRankingItem>, | ||||
|     val eventBannerList: GetEventResponse | ||||
| ) | ||||
| @@ -0,0 +1,22 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.free | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/v2/audio-content/main/free") | ||||
| class AudioContentMainTabFreeController(private val service: AudioContentMainTabFreeService) { | ||||
|     @GetMapping | ||||
|     fun fetchContentMainFree( | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok(service.fetchData(member)) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,91 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.free | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.AudioContentRepository | ||||
| import kr.co.vividnext.sodalive.content.ContentType | ||||
| import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService | ||||
| import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryRepository | ||||
| import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse | ||||
| import kr.co.vividnext.sodalive.content.main.tab.RecommendSeriesRepository | ||||
| import kr.co.vividnext.sodalive.content.theme.AudioContentThemeQueryRepository | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.stereotype.Service | ||||
|  | ||||
| @Service | ||||
| class AudioContentMainTabFreeService( | ||||
|     private val bannerService: AudioContentBannerService, | ||||
|     private val recommendSeriesRepository: RecommendSeriesRepository, | ||||
|     private val curationRepository: AudioContentCurationQueryRepository, | ||||
|     private val contentRepository: AudioContentRepository, | ||||
|     private val audioContentRepository: AudioContentRepository, | ||||
|     private val audioContentThemeRepository: AudioContentThemeQueryRepository | ||||
| ) { | ||||
|     fun fetchData(member: Member): GetContentMainTabFreeResponse { | ||||
|         val isAdult = member.auth != null | ||||
|         val memberId = member.id!! | ||||
|         val tabId = 7L | ||||
|  | ||||
|         val contentBannerList = bannerService.getBannerList( | ||||
|             tabId = tabId, | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult | ||||
|         ) | ||||
|  | ||||
|         val introduceCreator = curationRepository.findByContentMainTabIdAndTitle( | ||||
|             tabId = tabId, | ||||
|             title = "크리에이터 소개", | ||||
|             isAdult = isAdult | ||||
|         ) | ||||
|             .map { | ||||
|                 GetContentCurationResponse( | ||||
|                     title = it.title, | ||||
|                     items = contentRepository.findAudioContentByCurationId( | ||||
|                         curationId = it.id!!, | ||||
|                         isAdult = isAdult, | ||||
|                         contentType = ContentType.ALL | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|         val recommendSeriesList = recommendSeriesRepository.getRecommendSeriesList(isAdult = isAdult) | ||||
|  | ||||
|         val themeList = audioContentThemeRepository.getActiveThemeOfContent(isAdult = isAdult) | ||||
|         val newFreeContentList = if (themeList.isNotEmpty()) { | ||||
|             audioContentRepository.findByTheme( | ||||
|                 memberId = member.id!!, | ||||
|                 theme = themeList[0], | ||||
|                 isAdult = member.auth != null, | ||||
|                 contentType = ContentType.ALL, | ||||
|                 offset = 0, | ||||
|                 limit = 10, | ||||
|                 isFree = true | ||||
|             ) | ||||
|         } else { | ||||
|             emptyList() | ||||
|         } | ||||
|  | ||||
|         val curationList = curationRepository.findByContentMainTabId(tabId = tabId, isAdult = isAdult) | ||||
|             .map { | ||||
|                 GetContentCurationResponse( | ||||
|                     title = it.title, | ||||
|                     items = contentRepository.findAudioContentByCurationId( | ||||
|                         curationId = it.id!!, | ||||
|                         isAdult = isAdult, | ||||
|                         contentType = ContentType.ALL | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|         return GetContentMainTabFreeResponse( | ||||
|             contentBannerList = contentBannerList, | ||||
|             introduceCreator = if (introduceCreator.isNotEmpty()) { | ||||
|                 introduceCreator[0] | ||||
|             } else { | ||||
|                 null | ||||
|             }, | ||||
|             recommendSeriesList = recommendSeriesList, | ||||
|             themeList = themeList, | ||||
|             newFreeContentList = newFreeContentList, | ||||
|             curationList = curationList | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.free | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem | ||||
| import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse | ||||
| import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse | ||||
| import kr.co.vividnext.sodalive.content.main.tab.GetRecommendSeriesListResponse | ||||
|  | ||||
| data class GetContentMainTabFreeResponse( | ||||
|     val tabId: Long = 7, | ||||
|     val contentBannerList: List<GetAudioContentBannerResponse>, | ||||
|     val introduceCreator: GetContentCurationResponse?, | ||||
|     val recommendSeriesList: List<GetRecommendSeriesListResponse>, | ||||
|     val themeList: List<String>, | ||||
|     val newFreeContentList: List<GetAudioContentMainItem>, | ||||
|     val curationList: List<GetContentCurationResponse> | ||||
| ) | ||||
| @@ -0,0 +1,22 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.home | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/v2/audio-content/main/home") | ||||
| class AudioContentMainTabHomeController(private val service: AudioContentMainTabHomeService) { | ||||
|     @GetMapping | ||||
|     fun fetchContentMainHome( | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok(service.fetchData(member)) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,117 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.home | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService | ||||
| import kr.co.vividnext.sodalive.event.EventService | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import kr.co.vividnext.sodalive.notice.ServiceNoticeService | ||||
| import kr.co.vividnext.sodalive.rank.RankingService | ||||
| import org.springframework.stereotype.Service | ||||
| import java.time.DayOfWeek | ||||
| import java.time.LocalDateTime | ||||
| import java.time.format.DateTimeFormatter | ||||
| import java.time.temporal.TemporalAdjusters | ||||
|  | ||||
| @Service | ||||
| class AudioContentMainTabHomeService( | ||||
|     private val noticeService: ServiceNoticeService, | ||||
|     private val bannerService: AudioContentBannerService, | ||||
|     private val rankingService: RankingService, | ||||
|     private val eventService: EventService | ||||
| ) { | ||||
|     fun fetchData(member: Member): GetContentMainTabHomeResponse { | ||||
|         // 주간 랭킹 기간 | ||||
|         val currentDateTime = LocalDateTime.now() | ||||
|         val startDate = currentDateTime | ||||
|             .withHour(15) | ||||
|             .withMinute(0) | ||||
|             .withSecond(0) | ||||
|             .minusWeeks(1) | ||||
|             .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) | ||||
|         val endDate = startDate | ||||
|             .plusDays(6) | ||||
|  | ||||
|         val startDateFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일") | ||||
|         val endDateFormatter = DateTimeFormatter.ofPattern("MM월 dd일") | ||||
|  | ||||
|         val formattedLastMonday = startDate.format(startDateFormatter) | ||||
|         val formattedLastSunday = endDate.format(endDateFormatter) | ||||
|  | ||||
|         // 최근 공지사항 | ||||
|         val latestNotice = noticeService.getLatestNotice() | ||||
|  | ||||
|         // 메인 배너 (홈) | ||||
|         val contentBannerList = bannerService.getBannerList( | ||||
|             tabId = 1, | ||||
|             memberId = member.id!!, | ||||
|             isAdult = member.auth != null | ||||
|         ) | ||||
|  | ||||
|         // 인기 크리에이터 | ||||
|         val rankCreatorList = rankingService.getCreatorRanking( | ||||
|             memberId = member.id!!, | ||||
|             rankingDate = "$formattedLastMonday ~ $formattedLastSunday" | ||||
|         ) | ||||
|  | ||||
|         // 인기 시리즈 | ||||
|         val rankSeriesList = rankingService.getSeriesRanking( | ||||
|             memberId = member.id!!, | ||||
|             isAdult = member.auth != null, | ||||
|             startDate = startDate.minusDays(1), | ||||
|             endDate = endDate | ||||
|         ) | ||||
|  | ||||
|         // 인기 콘텐츠 | ||||
|         val rankContentList = rankingService.getContentRanking( | ||||
|             memberId = member.id!!, | ||||
|             isAdult = member.auth != null, | ||||
|             startDate = startDate.minusDays(1), | ||||
|             endDate = endDate | ||||
|         ) | ||||
|  | ||||
|         // 이벤트 배너 | ||||
|         val eventBannerList = eventService.getEventList(isAdult = member.auth != null) | ||||
|  | ||||
|         /* 채널별 인기 콘텐츠 | ||||
|          * - 콘텐츠를 4개 이상 등록한 채널 | ||||
|          * - 주간 콘텐츠 매출 Top 20 채널 | ||||
|          * - 해당 채널의 누적 매출 Top 2 | ||||
|          * - 해당 채널의 누적 판매 개수 Top 2 | ||||
|          */ | ||||
|         val contentRankCreatorList = rankingService.fetchCreatorByContentRevenueRankTop20( | ||||
|             memberId = member.id!!, | ||||
|             startDate = startDate.minusDays(1), | ||||
|             endDate = endDate | ||||
|         ) | ||||
|  | ||||
|         val salesRankContentList = if (contentRankCreatorList.isNotEmpty()) { | ||||
|             rankingService.fetchCreatorContentBySalesTop2( | ||||
|                 creatorId = contentRankCreatorList[0].creatorId, | ||||
|                 isAdult = member.auth != null | ||||
|             ) | ||||
|         } else { | ||||
|             emptyList() | ||||
|         } | ||||
|  | ||||
|         val salesCountRankContentList = if (contentRankCreatorList.isNotEmpty()) { | ||||
|             rankingService.fetchCreatorContentBySalesCountTop2( | ||||
|                 creatorId = contentRankCreatorList[0].creatorId, | ||||
|                 isAdult = member.auth != null | ||||
|             ) | ||||
|         } else { | ||||
|             emptyList() | ||||
|         } | ||||
|  | ||||
|         return GetContentMainTabHomeResponse( | ||||
|             latestNotice = latestNotice, | ||||
|             bannerList = contentBannerList, | ||||
|             rankCreatorList = rankCreatorList, | ||||
|             rankSeriesList = rankSeriesList, | ||||
|             rankSortTypeList = listOf("매출", "댓글", "좋아요"), | ||||
|             rankContentList = rankContentList, | ||||
|             eventBannerList = eventBannerList, | ||||
|             contentRankCreatorList = contentRankCreatorList, | ||||
|             salesRankContentList = salesRankContentList, | ||||
|             salesCountRankContentList = salesCountRankContentList | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.home | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.ContentCreatorResponse | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem | ||||
| import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse | ||||
| import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse | ||||
| import kr.co.vividnext.sodalive.event.GetEventResponse | ||||
| import kr.co.vividnext.sodalive.explorer.GetExplorerSectionResponse | ||||
| import kr.co.vividnext.sodalive.notice.NoticeTitleItem | ||||
|  | ||||
| data class GetContentMainTabHomeResponse( | ||||
|     val tabId: Long = 1, | ||||
|     val latestNotice: NoticeTitleItem?, | ||||
|     val bannerList: List<GetAudioContentBannerResponse>, | ||||
|     val rankCreatorList: GetExplorerSectionResponse, | ||||
|     val rankSeriesList: List<GetSeriesListResponse.SeriesListItem>, | ||||
|     val rankSortTypeList: List<String>, | ||||
|     val rankContentList: List<GetAudioContentRankingItem>, | ||||
|     val eventBannerList: GetEventResponse, | ||||
|     val contentRankCreatorList: List<ContentCreatorResponse>, | ||||
|     val salesRankContentList: List<GetAudioContentRankingItem>, | ||||
|     val salesCountRankContentList: List<GetAudioContentRankingItem> | ||||
| ) | ||||
| @@ -0,0 +1,22 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.replay | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/v2/audio-content/main/replay") | ||||
| class AudioContentMainTabLiveReplayController(private val service: AudioContentMainTabLiveReplayService) { | ||||
|     @GetMapping | ||||
|     fun fetchContentMainTabLiveReplay( | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok(service.fetchData(member)) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,84 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.replay | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.AudioContentRepository | ||||
| import kr.co.vividnext.sodalive.content.ContentType | ||||
| import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService | ||||
| import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryRepository | ||||
| import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse | ||||
| import kr.co.vividnext.sodalive.event.EventService | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import kr.co.vividnext.sodalive.rank.RankingService | ||||
| import org.springframework.stereotype.Service | ||||
| import java.time.DayOfWeek | ||||
| import java.time.LocalDateTime | ||||
| import java.time.temporal.TemporalAdjusters | ||||
|  | ||||
| @Service | ||||
| class AudioContentMainTabLiveReplayService( | ||||
|     private val bannerService: AudioContentBannerService, | ||||
|     private val contentRepository: AudioContentRepository, | ||||
|     private val rankingService: RankingService, | ||||
|     private val eventService: EventService, | ||||
|     private val curationRepository: AudioContentCurationQueryRepository | ||||
| ) { | ||||
|     fun fetchData(member: Member): GetContentMainTabLiveReplayResponse { | ||||
|         val isAdult = member.auth != null | ||||
|         val memberId = member.id!! | ||||
|         val theme = "다시듣기" | ||||
|         val tabId = 6L | ||||
|  | ||||
|         val contentBannerList = bannerService.getBannerList( | ||||
|             tabId = tabId, | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult | ||||
|         ) | ||||
|  | ||||
|         val newLiveReplayContentList = contentRepository.findByTheme( | ||||
|             memberId = memberId, | ||||
|             theme = theme, | ||||
|             isAdult = isAdult, | ||||
|             contentType = ContentType.ALL, | ||||
|             limit = 10 | ||||
|         ) | ||||
|  | ||||
|         val currentDateTime = LocalDateTime.now() | ||||
|         val startDate = currentDateTime | ||||
|             .withHour(15) | ||||
|             .withMinute(0) | ||||
|             .withSecond(0) | ||||
|             .minusWeeks(1) | ||||
|             .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) | ||||
|         val endDate = startDate | ||||
|             .plusDays(6) | ||||
|  | ||||
|         val rankLiveReplayContentList = rankingService.getContentRanking( | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult, | ||||
|             startDate = startDate, | ||||
|             endDate = endDate, | ||||
|             theme = theme | ||||
|         ) | ||||
|  | ||||
|         val eventBannerList = eventService.getEventList(isAdult = isAdult) | ||||
|  | ||||
|         val curationList = curationRepository.findByContentMainTabId(tabId = tabId, isAdult = isAdult) | ||||
|             .map { | ||||
|                 GetContentCurationResponse( | ||||
|                     title = it.title, | ||||
|                     items = contentRepository.findAudioContentByCurationId( | ||||
|                         curationId = it.id!!, | ||||
|                         isAdult = isAdult, | ||||
|                         contentType = ContentType.ALL | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|         return GetContentMainTabLiveReplayResponse( | ||||
|             contentBannerList = contentBannerList, | ||||
|             newLiveReplayContentList = newLiveReplayContentList, | ||||
|             rankLiveReplayContentList = rankLiveReplayContentList, | ||||
|             eventBannerList = eventBannerList, | ||||
|             curationList = curationList | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.replay | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem | ||||
| import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse | ||||
| import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse | ||||
| import kr.co.vividnext.sodalive.event.GetEventResponse | ||||
|  | ||||
| data class GetContentMainTabLiveReplayResponse( | ||||
|     val tabId: Long = 6, | ||||
|     val contentBannerList: List<GetAudioContentBannerResponse>, | ||||
|     val newLiveReplayContentList: List<GetAudioContentMainItem>, | ||||
|     val rankLiveReplayContentList: List<GetAudioContentRankingItem>, | ||||
|     val eventBannerList: GetEventResponse, | ||||
|     val curationList: List<GetContentCurationResponse> | ||||
| ) | ||||
| @@ -0,0 +1,22 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.series | ||||
|  | ||||
| import kr.co.vividnext.sodalive.common.ApiResponse | ||||
| import kr.co.vividnext.sodalive.common.SodaException | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import org.springframework.security.core.annotation.AuthenticationPrincipal | ||||
| import org.springframework.web.bind.annotation.GetMapping | ||||
| import org.springframework.web.bind.annotation.RequestMapping | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
|  | ||||
| @RestController | ||||
| @RequestMapping("/v2/audio-content/main/series") | ||||
| class AudioContentMainTabSeriesController(private val service: AudioContentMainTabSeriesService) { | ||||
|     @GetMapping | ||||
|     fun fetchContentMainSeries( | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||
|     ) = run { | ||||
|         if (member == null) throw SodaException("로그인 정보를 확인해주세요.") | ||||
|  | ||||
|         ApiResponse.ok(service.fetchData(member)) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,149 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.series | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService | ||||
| import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryRepository | ||||
| import kr.co.vividnext.sodalive.content.main.tab.RecommendSeriesRepository | ||||
| import kr.co.vividnext.sodalive.content.series.ContentSeriesService | ||||
| import kr.co.vividnext.sodalive.event.EventService | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import kr.co.vividnext.sodalive.rank.RankingService | ||||
| import org.springframework.stereotype.Service | ||||
| import java.time.DayOfWeek | ||||
| import java.time.LocalDateTime | ||||
| import java.time.temporal.TemporalAdjusters | ||||
|  | ||||
| @Service | ||||
| class AudioContentMainTabSeriesService( | ||||
|     private val bannerService: AudioContentBannerService, | ||||
|     private val seriesService: ContentSeriesService, | ||||
|     private val rankingService: RankingService, | ||||
|     private val recommendSeriesRepository: RecommendSeriesRepository, | ||||
|     private val eventService: EventService, | ||||
|     private val curationRepository: AudioContentCurationQueryRepository | ||||
| ) { | ||||
|     fun fetchData(member: Member): GetContentMainTabSeriesResponse { | ||||
|         val isAdult = member.auth != null | ||||
|  | ||||
|         // 메인 배너 (시리즈) | ||||
|         val contentBannerList = bannerService.getBannerList( | ||||
|             tabId = 2, | ||||
|             memberId = member.id!!, | ||||
|             isAdult = isAdult | ||||
|         ) | ||||
|  | ||||
|         val originalAudioDrama = seriesService.getOriginalAudioDramaList( | ||||
|             isAdult = isAdult, | ||||
|             offset = 0, | ||||
|             limit = 20 | ||||
|         ) | ||||
|  | ||||
|         // 일간 랭킹 | ||||
|         val currentDateTime = LocalDateTime.now() | ||||
|         val dailyRankingStartDate = currentDateTime | ||||
|             .withHour(15) | ||||
|             .withMinute(0) | ||||
|             .withSecond(0) | ||||
|             .minusDays(2) | ||||
|         val dailyRankingEndDate = dailyRankingStartDate | ||||
|             .plusDays(1) | ||||
|  | ||||
|         val rankSeriesList = rankingService.getSeriesRanking( | ||||
|             memberId = member.id!!, | ||||
|             isAdult = isAdult, | ||||
|             startDate = dailyRankingStartDate, | ||||
|             endDate = dailyRankingEndDate | ||||
|         ) | ||||
|  | ||||
|         // 시리즈 장르 | ||||
|         val genreList = seriesService.getGenreList(isAdult = isAdult) | ||||
|  | ||||
|         // 장르별 추천 시리즈 | ||||
|         val recommendSeriesList = if (genreList.isNotEmpty()) { | ||||
|             rankingService.getSeriesAllRankingByGenre( | ||||
|                 memberId = member.id!!, | ||||
|                 isAdult = isAdult, | ||||
|                 genreId = genreList[0].id | ||||
|             ) | ||||
|         } else { | ||||
|             emptyList() | ||||
|         } | ||||
|  | ||||
|         // 새로운 시리즈 | ||||
|         val newSeriesList = recommendSeriesRepository.getNewSeriesList(isAdult = isAdult) | ||||
|  | ||||
|         // 완결 시리즈 월간 랭킹 | ||||
|         val monthlyRankingStartDate = currentDateTime | ||||
|             .withDayOfMonth(1) | ||||
|             .withHour(15) | ||||
|             .withMinute(0) | ||||
|             .withSecond(0) | ||||
|             .minusDays(1) | ||||
|         val monthlyRankingEndDate = monthlyRankingStartDate | ||||
|             .plusMonths(1) | ||||
|  | ||||
|         val rankCompleteSeriesList = rankingService.getCompleteSeriesRanking( | ||||
|             memberId = member.id!!, | ||||
|             isAdult = isAdult, | ||||
|             startDate = monthlyRankingStartDate, | ||||
|             endDate = monthlyRankingEndDate | ||||
|         ) | ||||
|  | ||||
|         val startDate = currentDateTime | ||||
|             .withHour(15) | ||||
|             .withMinute(0) | ||||
|             .withSecond(0) | ||||
|             .minusWeeks(1) | ||||
|             .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) | ||||
|         val endDate = startDate | ||||
|             .plusDays(6) | ||||
|  | ||||
|         val seriesRankCreatorList = rankingService.fetchCreatorBySeriesRevenueRankTop20( | ||||
|             memberId = member.id!!, | ||||
|             startDate = startDate.minusDays(1), | ||||
|             endDate = endDate | ||||
|         ) | ||||
|  | ||||
|         val salesRankContentList = if (seriesRankCreatorList.isNotEmpty()) { | ||||
|             rankingService.fetchCreatorSeriesBySales( | ||||
|                 creatorId = seriesRankCreatorList[0].creatorId, | ||||
|                 isAdult = isAdult | ||||
|             ) | ||||
|         } else { | ||||
|             emptyList() | ||||
|         } | ||||
|  | ||||
|         // 이벤트 배너 | ||||
|         val eventBannerList = eventService.getEventList(isAdult = isAdult) | ||||
|  | ||||
|         // 큐레이션 | ||||
|         val curationList = curationRepository.findByContentMainTabId(tabId = 2, isAdult = isAdult) | ||||
|             .map { | ||||
|                 GetSeriesCurationResponse( | ||||
|                     title = it.title, | ||||
|                     items = seriesService.fetchSeriesByCurationId( | ||||
|                         curationId = it.id!!, | ||||
|                         memberId = member.id!!, | ||||
|                         isAdult = isAdult | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|         return GetContentMainTabSeriesResponse( | ||||
|             contentBannerList = contentBannerList, | ||||
|             originalAudioDrama = if (originalAudioDrama.size >= 3) { | ||||
|                 originalAudioDrama | ||||
|             } else { | ||||
|                 emptyList() | ||||
|             }, | ||||
|             rankSeriesList = rankSeriesList, | ||||
|             genreList = genreList, | ||||
|             recommendSeriesList = recommendSeriesList, | ||||
|             newSeriesList = newSeriesList, | ||||
|             rankCompleteSeriesList = rankCompleteSeriesList, | ||||
|             seriesRankCreatorList = seriesRankCreatorList, | ||||
|             salesRankContentList = salesRankContentList, | ||||
|             eventBannerList = eventBannerList, | ||||
|             curationList = curationList | ||||
|         ) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,23 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.series | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.ContentCreatorResponse | ||||
| import kr.co.vividnext.sodalive.content.main.banner.GetAudioContentBannerResponse | ||||
| import kr.co.vividnext.sodalive.content.main.tab.GetRecommendSeriesListResponse | ||||
| import kr.co.vividnext.sodalive.content.series.GetSeriesGenreListResponse | ||||
| import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse | ||||
| import kr.co.vividnext.sodalive.event.GetEventResponse | ||||
|  | ||||
| data class GetContentMainTabSeriesResponse( | ||||
|     val tabId: Long = 2, | ||||
|     val contentBannerList: List<GetAudioContentBannerResponse>, | ||||
|     val originalAudioDrama: List<GetSeriesListResponse.SeriesListItem>, | ||||
|     val rankSeriesList: List<GetSeriesListResponse.SeriesListItem>, | ||||
|     val genreList: List<GetSeriesGenreListResponse>, | ||||
|     val recommendSeriesList: List<GetSeriesListResponse.SeriesListItem>, | ||||
|     val newSeriesList: List<GetRecommendSeriesListResponse>, | ||||
|     val rankCompleteSeriesList: List<GetSeriesListResponse.SeriesListItem>, | ||||
|     val seriesRankCreatorList: List<ContentCreatorResponse>, | ||||
|     val salesRankContentList: List<GetSeriesListResponse.SeriesListItem>, | ||||
|     val eventBannerList: GetEventResponse, | ||||
|     val curationList: List<GetSeriesCurationResponse> | ||||
| ) | ||||
| @@ -0,0 +1,8 @@ | ||||
| package kr.co.vividnext.sodalive.content.main.tab.series | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse | ||||
|  | ||||
| data class GetSeriesCurationResponse( | ||||
|     val title: String, | ||||
|     val items: List<GetSeriesListResponse.SeriesListItem> | ||||
| ) | ||||
| @@ -2,15 +2,21 @@ package kr.co.vividnext.sodalive.content.series | ||||
|  | ||||
| import com.querydsl.core.types.dsl.Expressions | ||||
| import com.querydsl.jpa.impl.JPAQueryFactory | ||||
| import kr.co.vividnext.sodalive.admin.content.series.genre.QSeriesGenre.seriesGenre | ||||
| import kr.co.vividnext.sodalive.content.ContentType | ||||
| import kr.co.vividnext.sodalive.content.QAudioContent.audioContent | ||||
| import kr.co.vividnext.sodalive.content.hashtag.QHashTag.hashTag | ||||
| import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration | ||||
| import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCurationItem.audioContentCurationItem | ||||
| import kr.co.vividnext.sodalive.content.series.content.GetSeriesContentMinMaxPriceResponse | ||||
| import kr.co.vividnext.sodalive.content.series.content.QGetSeriesContentMinMaxPriceResponse | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.Series | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.keyword.QSeriesKeyword.seriesKeyword | ||||
| import kr.co.vividnext.sodalive.member.MemberRole | ||||
| import kr.co.vividnext.sodalive.member.QMember.member | ||||
| import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember | ||||
| import org.springframework.data.jpa.repository.JpaRepository | ||||
|  | ||||
| interface ContentSeriesRepository : JpaRepository<Series, Long>, ContentSeriesQueryRepository | ||||
| @@ -29,6 +35,9 @@ interface ContentSeriesQueryRepository { | ||||
|     fun getKeywordList(seriesId: Long): List<String> | ||||
|     fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse | ||||
|     fun getRecommendSeriesList(isAuth: Boolean, contentType: ContentType, limit: Long): List<Series> | ||||
|     fun getOriginalAudioDramaList(isAdult: Boolean, offset: Long = 0, limit: Long = 20): List<Series> | ||||
|     fun getGenreList(isAdult: Boolean): List<GetSeriesGenreListResponse> | ||||
|     fun findByCurationId(curationId: Long, memberId: Long, isAdult: Boolean): List<Series> | ||||
| } | ||||
|  | ||||
| class ContentSeriesQueryRepositoryImpl( | ||||
| @@ -133,4 +142,61 @@ class ContentSeriesQueryRepositoryImpl( | ||||
|             .limit(limit) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     override fun getOriginalAudioDramaList(isAdult: Boolean, offset: Long, limit: Long): List<Series> { | ||||
|         var where = series.isOriginal.isTrue | ||||
|             .and(series.isActive.isTrue) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .selectFrom(series) | ||||
|             .where(where) | ||||
|             .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) | ||||
|             .offset(offset) | ||||
|             .limit(limit) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     override fun getGenreList(isAdult: Boolean): List<GetSeriesGenreListResponse> { | ||||
|         var where = seriesGenre.isActive.isTrue | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(seriesGenre.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select(QGetSeriesGenreListResponse(seriesGenre.id, seriesGenre.genre)) | ||||
|             .from(seriesGenre) | ||||
|             .where(where) | ||||
|             .orderBy(seriesGenre.orders.asc()) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     override fun findByCurationId(curationId: Long, memberId: Long, isAdult: Boolean): List<Series> { | ||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) | ||||
|             .and(blockMember.isActive.isTrue) | ||||
|             .and(blockMember.blockedMember.id.eq(memberId)) | ||||
|  | ||||
|         var where = series.isActive.isTrue | ||||
|             .and(member.isActive.isTrue) | ||||
|             .and(member.role.eq(MemberRole.CREATOR)) | ||||
|             .and(blockMember.id.isNull) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select(series) | ||||
|             .from(audioContentCurationItem) | ||||
|             .innerJoin(audioContentCurationItem.curation, audioContentCuration) | ||||
|             .innerJoin(audioContentCurationItem.series, series) | ||||
|             .innerJoin(series.member, member) | ||||
|             .leftJoin(blockMember).on(blockMemberCondition) | ||||
|             .where(where) | ||||
|             .fetch() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -30,6 +30,19 @@ class ContentSeriesService( | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     private val coverImageHost: String | ||||
| ) { | ||||
|     fun getOriginalAudioDramaList( | ||||
|         isAdult: Boolean, | ||||
|         offset: Long = 0, | ||||
|         limit: Long = 20 | ||||
|     ): List<GetSeriesListResponse.SeriesListItem> { | ||||
|         val originalAudioDramaList = repository.getOriginalAudioDramaList(isAdult, offset, limit) | ||||
|         return seriesToSeriesListItem(originalAudioDramaList, isAdult) | ||||
|     } | ||||
|  | ||||
|     fun getGenreList(isAdult: Boolean): List<GetSeriesGenreListResponse> { | ||||
|         return repository.getGenreList(isAdult = isAdult) | ||||
|     } | ||||
|  | ||||
|     fun getSeriesList( | ||||
|         creatorId: Long, | ||||
|         member: Member, | ||||
| @@ -170,6 +183,15 @@ class ContentSeriesService( | ||||
|         return seriesToSeriesListItem(seriesList = seriesList, isAdult = member.auth != null) | ||||
|     } | ||||
|  | ||||
|     fun fetchSeriesByCurationId( | ||||
|         curationId: Long, | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean | ||||
|     ): List<GetSeriesListResponse.SeriesListItem> { | ||||
|         val seriesList = repository.findByCurationId(curationId = curationId, memberId = memberId, isAdult = isAdult) | ||||
|         return seriesToSeriesListItem(seriesList, isAdult) | ||||
|     } | ||||
|  | ||||
|     private fun seriesToSeriesListItem( | ||||
|         seriesList: List<Series>, | ||||
|         isAdult: Boolean | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| package kr.co.vividnext.sodalive.content.series | ||||
|  | ||||
| import com.querydsl.core.annotations.QueryProjection | ||||
|  | ||||
| data class GetSeriesGenreListResponse @QueryProjection constructor( | ||||
|     val id: Long, | ||||
|     val genre: String | ||||
| ) | ||||
| @@ -6,19 +6,13 @@ import kr.co.vividnext.sodalive.content.ContentType | ||||
| import kr.co.vividnext.sodalive.content.SortType | ||||
| import kr.co.vividnext.sodalive.content.theme.content.GetContentByThemeResponse | ||||
| 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 | ||||
|  | ||||
| @Service | ||||
| class AudioContentThemeService( | ||||
|     private val queryRepository: AudioContentThemeQueryRepository, | ||||
|     private val blockMemberRepository: BlockMemberRepository, | ||||
|     private val contentRepository: AudioContentRepository, | ||||
|  | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     private val imageHost: String | ||||
|     private val contentRepository: AudioContentRepository | ||||
| ) { | ||||
|     @Transactional(readOnly = true) | ||||
|     fun getThemes(): List<GetAudioContentThemeResponse> { | ||||
| @@ -46,7 +40,6 @@ class AudioContentThemeService( | ||||
|         ) | ||||
|  | ||||
|         val items = contentRepository.findByTheme( | ||||
|             cloudfrontHost = imageHost, | ||||
|             memberId = member.id!!, | ||||
|             theme = theme.theme, | ||||
|             sortType = sortType, | ||||
| @@ -55,9 +48,6 @@ class AudioContentThemeService( | ||||
|             offset = offset, | ||||
|             limit = limit | ||||
|         ) | ||||
|             .asSequence() | ||||
|             .filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.creatorId) } | ||||
|             .toList() | ||||
|  | ||||
|         return GetContentByThemeResponse( | ||||
|             theme = theme.theme, | ||||
|   | ||||
| @@ -24,10 +24,8 @@ class AuthController(private val service: AuthService) { | ||||
|         if (service.isBlockAuth(authenticateData)) { | ||||
|             service.signOut(member.id!!) | ||||
|             throw SodaException("운영정책을 위반하여 이용을 제한합니다.") | ||||
|         } else { | ||||
|             service.authenticate(authenticateData, member.id!!) | ||||
|         } | ||||
|  | ||||
|         ApiResponse.ok(null, null) | ||||
|         ApiResponse.ok(service.authenticate(authenticateData, member.id!!)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,3 @@ | ||||
| package kr.co.vividnext.sodalive.member.auth | ||||
|  | ||||
| data class AuthResponse(val gender: Int) | ||||
| @@ -73,7 +73,7 @@ class AuthService( | ||||
|     } | ||||
|  | ||||
|     @Transactional | ||||
|     fun authenticate(certificate: AuthVerifyCertificate, memberId: Long) { | ||||
|     fun authenticate(certificate: AuthVerifyCertificate, memberId: Long): AuthResponse { | ||||
|         val memberIds = repository.getActiveMemberIdsByDi(di = certificate.di) | ||||
|         if (memberIds.size >= 3) { | ||||
|             throw SodaException( | ||||
| @@ -96,8 +96,9 @@ class AuthService( | ||||
|             auth.member = member | ||||
|  | ||||
|             repository.save(auth) | ||||
|             return AuthResponse(gender = certificate.gender) | ||||
|         } else { | ||||
|             throw SodaException("2005년 1월 1일 이전 출생자만 본인인증이 가능합니다.") | ||||
|             throw SodaException("${nowYear - 19}년 1월 1일 이전 출생자만 본인인증이 가능합니다.") | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| package kr.co.vividnext.sodalive.notice | ||||
|  | ||||
| import com.querydsl.core.annotations.QueryProjection | ||||
|  | ||||
| data class GetNoticeResponse( | ||||
|     val totalCount: Int, | ||||
|     val noticeList: List<NoticeItem> | ||||
| @@ -11,3 +13,8 @@ data class NoticeItem( | ||||
|     val content: String, | ||||
|     val date: String | ||||
| ) | ||||
|  | ||||
| data class NoticeTitleItem @QueryProjection constructor( | ||||
|     val id: Long, | ||||
|     val title: String | ||||
| ) | ||||
|   | ||||
| @@ -63,4 +63,8 @@ class ServiceNoticeService(private val repository: ServiceServiceNoticeRepositor | ||||
|  | ||||
|         return GetNoticeResponse(totalCount, noticeList) | ||||
|     } | ||||
|  | ||||
|     fun getLatestNotice(): NoticeTitleItem? { | ||||
|         return repository.getLatestNotice() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ interface ServiceServiceNoticeRepository : JpaRepository<ServiceNotice, Long>, S | ||||
| interface ServiceNoticeQueryRepository { | ||||
|     fun getNoticeTotalCount(): Int | ||||
|     fun getNoticeList(pageable: Pageable): List<ServiceNotice> | ||||
|     fun getLatestNotice(): NoticeTitleItem? | ||||
| } | ||||
|  | ||||
| @Repository | ||||
| @@ -34,4 +35,18 @@ class ServiceNoticeQueryRepositoryImpl(private val queryFactory: JPAQueryFactory | ||||
|             .orderBy(serviceNotice.id.desc()) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     override fun getLatestNotice(): NoticeTitleItem? { | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QNoticeTitleItem( | ||||
|                     serviceNotice.id, | ||||
|                     serviceNotice.title | ||||
|                 ) | ||||
|             ) | ||||
|             .from(serviceNotice) | ||||
|             .where(serviceNotice.isActive.isTrue) | ||||
|             .orderBy(serviceNotice.id.desc()) | ||||
|             .fetchFirst() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,497 @@ | ||||
| package kr.co.vividnext.sodalive.rank | ||||
|  | ||||
| import com.querydsl.core.types.dsl.Expressions | ||||
| import com.querydsl.jpa.impl.JPAQueryFactory | ||||
| import kr.co.vividnext.sodalive.admin.content.series.genre.QSeriesGenre.seriesGenre | ||||
| import kr.co.vividnext.sodalive.content.QAudioContent.audioContent | ||||
| import kr.co.vividnext.sodalive.content.comment.QAudioContentComment.audioContentComment | ||||
| import kr.co.vividnext.sodalive.content.like.QAudioContentLike.audioContentLike | ||||
| import kr.co.vividnext.sodalive.content.main.ContentCreatorResponse | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem | ||||
| import kr.co.vividnext.sodalive.content.main.QContentCreatorResponse | ||||
| import kr.co.vividnext.sodalive.content.main.QGetAudioContentRankingItem | ||||
| import kr.co.vividnext.sodalive.content.order.QOrder.order | ||||
| import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.Series | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState | ||||
| import kr.co.vividnext.sodalive.explorer.QCreatorRanking.creatorRanking | ||||
| import kr.co.vividnext.sodalive.member.Member | ||||
| import kr.co.vividnext.sodalive.member.MemberRole | ||||
| import kr.co.vividnext.sodalive.member.QMember.member | ||||
| import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember | ||||
| import org.springframework.beans.factory.annotation.Value | ||||
| import org.springframework.stereotype.Repository | ||||
| import java.time.LocalDateTime | ||||
|  | ||||
| @Repository | ||||
| class RankingRepository( | ||||
|     private val queryFactory: JPAQueryFactory, | ||||
|  | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     private val imageHost: String | ||||
| ) { | ||||
|     fun getCreatorRankings(): List<Member> { | ||||
|         return queryFactory | ||||
|             .select(member) | ||||
|             .from(creatorRanking) | ||||
|             .innerJoin(creatorRanking.member, member) | ||||
|             .orderBy(creatorRanking.ranking.asc()) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun getAudioContentRanking( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         startDate: LocalDateTime, | ||||
|         endDate: LocalDateTime, | ||||
|         offset: Long, | ||||
|         limit: Long, | ||||
|         sortType: String, | ||||
|         theme: String = "" | ||||
|     ): List<GetAudioContentRankingItem> { | ||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) | ||||
|             .and(blockMember.isActive.isTrue) | ||||
|             .and(blockMember.blockedMember.id.eq(memberId)) | ||||
|  | ||||
|         var where = audioContent.isActive.isTrue | ||||
|             .and(audioContent.member.isActive.isTrue) | ||||
|             .and(audioContent.member.isNotNull) | ||||
|             .and(audioContent.member.role.eq(MemberRole.CREATOR)) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
|             .and(audioContentTheme.isActive.isTrue) | ||||
|             .and(audioContent.limited.isNull) | ||||
|             .and(blockMember.id.isNull) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(audioContent.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         if (theme.isNotBlank()) { | ||||
|             where = where.and(audioContentTheme.theme.eq(theme)) | ||||
|         } | ||||
|  | ||||
|         var select = queryFactory | ||||
|             .select( | ||||
|                 QGetAudioContentRankingItem( | ||||
|                     audioContent.id, | ||||
|                     audioContent.title, | ||||
|                     audioContent.coverImage.prepend("/").prepend(imageHost), | ||||
|                     audioContentTheme.theme, | ||||
|                     audioContent.price, | ||||
|                     audioContent.duration, | ||||
|                     member.id, | ||||
|                     member.nickname | ||||
|                 ) | ||||
|             ) | ||||
|  | ||||
|         select = when (sortType) { | ||||
|             "후원" -> { | ||||
|                 select | ||||
|                     .from(audioContentComment) | ||||
|                     .innerJoin(audioContentComment.audioContent, audioContent) | ||||
|                     .innerJoin(audioContent.member, member) | ||||
|                     .innerJoin(audioContent.theme, audioContentTheme) | ||||
|                     .leftJoin(blockMember).on(blockMemberCondition) | ||||
|                     .where( | ||||
|                         where | ||||
|                             .and(audioContentComment.isActive.isTrue) | ||||
|                             .and(audioContentComment.donationCan.gt(0)) | ||||
|                             .and(audioContentComment.createdAt.goe(startDate)) | ||||
|                             .and(audioContentComment.createdAt.lt(endDate)) | ||||
|                     ) | ||||
|                     .groupBy(audioContent.id) | ||||
|                     .orderBy(audioContentComment.donationCan.sum().desc(), audioContent.createdAt.asc()) | ||||
|             } | ||||
|  | ||||
|             "댓글" -> { | ||||
|                 select | ||||
|                     .from(audioContentComment) | ||||
|                     .innerJoin(audioContentComment.audioContent, audioContent) | ||||
|                     .innerJoin(audioContent.member, member) | ||||
|                     .innerJoin(audioContent.theme, audioContentTheme) | ||||
|                     .leftJoin(blockMember).on(blockMemberCondition) | ||||
|                     .where( | ||||
|                         where | ||||
|                             .and(audioContentComment.isActive.isTrue) | ||||
|                             .and(audioContentComment.createdAt.goe(startDate)) | ||||
|                             .and(audioContentComment.createdAt.lt(endDate)) | ||||
|                     ) | ||||
|                     .groupBy(audioContentComment.audioContent.id) | ||||
|                     .orderBy(audioContentComment.id.count().desc(), audioContent.createdAt.asc()) | ||||
|             } | ||||
|  | ||||
|             "좋아요" -> { | ||||
|                 select | ||||
|                     .from(audioContentLike) | ||||
|                     .innerJoin(audioContentLike.audioContent, audioContent) | ||||
|                     .innerJoin(audioContent.member, member) | ||||
|                     .innerJoin(audioContent.theme, audioContentTheme) | ||||
|                     .leftJoin(blockMember).on(blockMemberCondition) | ||||
|                     .where( | ||||
|                         where | ||||
|                             .and(audioContentLike.isActive.isTrue) | ||||
|                             .and(audioContentLike.createdAt.goe(startDate)) | ||||
|                             .and(audioContentLike.createdAt.lt(endDate)) | ||||
|                     ) | ||||
|                     .groupBy(audioContentLike.audioContent.id) | ||||
|                     .orderBy(audioContentLike.id.count().desc(), audioContent.createdAt.asc()) | ||||
|             } | ||||
|  | ||||
|             else -> { | ||||
|                 select | ||||
|                     .from(order) | ||||
|                     .innerJoin(order.audioContent, audioContent) | ||||
|                     .innerJoin(audioContent.member, member) | ||||
|                     .innerJoin(audioContent.theme, audioContentTheme) | ||||
|                     .leftJoin(blockMember).on(blockMemberCondition) | ||||
|                     .where( | ||||
|                         where | ||||
|                             .and(order.isActive.isTrue) | ||||
|                             .and(order.createdAt.goe(startDate)) | ||||
|                             .and(order.createdAt.lt(endDate)) | ||||
|                     ) | ||||
|                     .groupBy(audioContent.id) | ||||
|                     .orderBy(order.can.sum().desc(), audioContent.createdAt.asc()) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return select | ||||
|             .offset(offset) | ||||
|             .limit(limit) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun getSeriesRanking( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         startDate: LocalDateTime, | ||||
|         endDate: LocalDateTime | ||||
|     ): List<Series> { | ||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) | ||||
|             .and(blockMember.isActive.isTrue) | ||||
|             .and(blockMember.blockedMember.id.eq(memberId)) | ||||
|  | ||||
|         var where = series.isActive.isTrue | ||||
|             .and(audioContent.isActive.isTrue) | ||||
|             .and(member.isActive.isTrue) | ||||
|             .and(member.isNotNull) | ||||
|             .and(member.role.eq(MemberRole.CREATOR)) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
|             .and(audioContent.limited.isNull) | ||||
|             .and(blockMember.id.isNull) | ||||
|             .and(order.isActive.isTrue) | ||||
|             .and(order.createdAt.goe(startDate)) | ||||
|             .and(order.createdAt.lt(endDate)) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select(series) | ||||
|             .from(seriesContent) | ||||
|             .innerJoin(seriesContent.series, series) | ||||
|             .innerJoin(seriesContent.content, audioContent) | ||||
|             .innerJoin(series.member, member) | ||||
|             .leftJoin(order).on(audioContent.id.eq(order.audioContent.id)) | ||||
|             .leftJoin(blockMember).on(blockMemberCondition) | ||||
|             .where(where) | ||||
|             .groupBy(series.id) | ||||
|             .orderBy( | ||||
|                 order.can.sum().desc(), | ||||
|                 Expressions.numberTemplate(Double::class.java, "function('rand')").asc() | ||||
|             ) | ||||
|             .offset(0) | ||||
|             .limit(10) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun getCompleteSeriesRanking( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         startDate: LocalDateTime, | ||||
|         endDate: LocalDateTime, | ||||
|         offset: Long, | ||||
|         limit: Long | ||||
|     ): List<Series> { | ||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) | ||||
|             .and(blockMember.isActive.isTrue) | ||||
|             .and(blockMember.blockedMember.id.eq(memberId)) | ||||
|  | ||||
|         var where = series.isActive.isTrue | ||||
|             .and(series.state.eq(SeriesState.COMPLETE)) | ||||
|             .and(audioContent.isActive.isTrue) | ||||
|             .and(member.isActive.isTrue) | ||||
|             .and(member.isNotNull) | ||||
|             .and(member.role.eq(MemberRole.CREATOR)) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
|             .and(audioContent.limited.isNull) | ||||
|             .and(blockMember.id.isNull) | ||||
|             .and(order.isActive.isTrue) | ||||
|             .and(order.createdAt.goe(startDate)) | ||||
|             .and(order.createdAt.lt(endDate)) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select(series) | ||||
|             .from(seriesContent) | ||||
|             .innerJoin(seriesContent.series, series) | ||||
|             .innerJoin(seriesContent.content, audioContent) | ||||
|             .innerJoin(series.member, member) | ||||
|             .leftJoin(order).on(audioContent.id.eq(order.audioContent.id)) | ||||
|             .leftJoin(blockMember).on(blockMemberCondition) | ||||
|             .where(where) | ||||
|             .groupBy(series.id) | ||||
|             .orderBy( | ||||
|                 order.can.sum().desc(), | ||||
|                 Expressions.numberTemplate(Double::class.java, "function('rand')").asc() | ||||
|             ) | ||||
|             .offset(offset) | ||||
|             .limit(limit) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun getSeriesAllRankingByGenre(memberId: Long, isAdult: Boolean, genreId: Long): List<Series> { | ||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) | ||||
|             .and(blockMember.isActive.isTrue) | ||||
|             .and(blockMember.blockedMember.id.eq(memberId)) | ||||
|  | ||||
|         var where = series.isActive.isTrue | ||||
|             .and(seriesGenre.id.eq(genreId)) | ||||
|             .and(audioContent.isActive.isTrue) | ||||
|             .and(member.isActive.isTrue) | ||||
|             .and(member.isNotNull) | ||||
|             .and(member.role.eq(MemberRole.CREATOR)) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
|             .and(audioContent.limited.isNull) | ||||
|             .and(blockMember.id.isNull) | ||||
|             .and(order.isActive.isTrue) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select(series) | ||||
|             .from(seriesContent) | ||||
|             .innerJoin(seriesContent.series, series) | ||||
|             .innerJoin(series.genre, seriesGenre) | ||||
|             .innerJoin(seriesContent.content, audioContent) | ||||
|             .innerJoin(series.member, member) | ||||
|             .leftJoin(order).on(audioContent.id.eq(order.audioContent.id)) | ||||
|             .leftJoin(blockMember).on(blockMemberCondition) | ||||
|             .where(where) | ||||
|             .groupBy(series.id) | ||||
|             .orderBy( | ||||
|                 order.can.sum().desc(), | ||||
|                 Expressions.numberTemplate(Double::class.java, "function('rand')").asc() | ||||
|             ) | ||||
|             .offset(0) | ||||
|             .limit(20) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun fetchCreatorByContentRevenueRankTop20( | ||||
|         memberId: Long, | ||||
|         startDate: LocalDateTime, | ||||
|         endDate: LocalDateTime | ||||
|     ): List<ContentCreatorResponse> { | ||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) | ||||
|             .and(blockMember.isActive.isTrue) | ||||
|             .and(blockMember.blockedMember.id.eq(memberId)) | ||||
|  | ||||
|         val ordersCondition = order.audioContent.id.eq(audioContent.id) | ||||
|             .and(order.isActive.isTrue) | ||||
|             .and(order.createdAt.goe(startDate)) | ||||
|             .and(order.createdAt.lt(startDate)) | ||||
|  | ||||
|         val where = member.isActive.isTrue | ||||
|             .and(member.role.eq(MemberRole.CREATOR)) | ||||
|             .and(audioContent.isActive.isTrue) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
|             .and(audioContent.limited.isNull) | ||||
|             .and(blockMember.id.isNull) | ||||
|  | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QContentCreatorResponse( | ||||
|                     member.id, | ||||
|                     member.nickname, | ||||
|                     member.profileImage.prepend("/").prepend(imageHost) | ||||
|                 ) | ||||
|             ) | ||||
|             .from(member) | ||||
|             .innerJoin(audioContent).on(member.id.eq(audioContent.member.id)) | ||||
|             .leftJoin(order).on(ordersCondition) | ||||
|             .leftJoin(blockMember).on(blockMemberCondition) | ||||
|             .where(where) | ||||
|             .groupBy(member.id) | ||||
|             .having(audioContent.id.count().goe(4)) | ||||
|             .orderBy( | ||||
|                 order.can.sum().desc(), | ||||
|                 Expressions.numberTemplate(Double::class.java, "function('rand')").asc() | ||||
|             ) | ||||
|             .offset(0) | ||||
|             .limit(20) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun fetchCreatorContentBySalesTop2(creatorId: Long, isAdult: Boolean): List<GetAudioContentRankingItem> { | ||||
|         var where = member.isActive.isTrue | ||||
|             .and(member.role.eq(MemberRole.CREATOR)) | ||||
|             .and(audioContent.isActive.isTrue) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
|             .and(audioContent.limited.isNull) | ||||
|             .and(order.isActive.isTrue) | ||||
|             .and(member.id.eq(creatorId)) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QGetAudioContentRankingItem( | ||||
|                     audioContent.id, | ||||
|                     audioContent.title, | ||||
|                     audioContent.coverImage.prepend("/").prepend(imageHost), | ||||
|                     audioContentTheme.theme, | ||||
|                     audioContent.price, | ||||
|                     audioContent.duration, | ||||
|                     member.id, | ||||
|                     member.nickname | ||||
|                 ) | ||||
|             ) | ||||
|             .from(order) | ||||
|             .innerJoin(order.audioContent, audioContent) | ||||
|             .innerJoin(audioContent.member, member) | ||||
|             .where(where) | ||||
|             .groupBy(audioContent.id) | ||||
|             .orderBy(order.can.sum().desc()) | ||||
|             .offset(0) | ||||
|             .limit(2) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun fetchCreatorContentBySalesCountTop2(creatorId: Long, isAdult: Boolean): List<GetAudioContentRankingItem> { | ||||
|         var where = member.isActive.isTrue | ||||
|             .and(member.role.eq(MemberRole.CREATOR)) | ||||
|             .and(audioContent.isActive.isTrue) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
|             .and(audioContent.limited.isNull) | ||||
|             .and(order.isActive.isTrue) | ||||
|             .and(member.id.eq(creatorId)) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QGetAudioContentRankingItem( | ||||
|                     audioContent.id, | ||||
|                     audioContent.title, | ||||
|                     audioContent.coverImage.prepend("/").prepend(imageHost), | ||||
|                     audioContentTheme.theme, | ||||
|                     audioContent.price, | ||||
|                     audioContent.duration, | ||||
|                     member.id, | ||||
|                     member.nickname | ||||
|                 ) | ||||
|             ) | ||||
|             .from(order) | ||||
|             .innerJoin(order.audioContent, audioContent) | ||||
|             .innerJoin(audioContent.member, member) | ||||
|             .where(where) | ||||
|             .groupBy(audioContent.id) | ||||
|             .orderBy(order.id.count().desc()) | ||||
|             .offset(0) | ||||
|             .limit(2) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun fetchCreatorBySeriesRevenueRankTop20( | ||||
|         memberId: Long, | ||||
|         startDate: LocalDateTime, | ||||
|         endDate: LocalDateTime | ||||
|     ): List<ContentCreatorResponse> { | ||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) | ||||
|             .and(blockMember.isActive.isTrue) | ||||
|             .and(blockMember.blockedMember.id.eq(memberId)) | ||||
|  | ||||
|         val ordersCondition = order.audioContent.id.eq(audioContent.id) | ||||
|             .and(order.isActive.isTrue) | ||||
|             .and(order.createdAt.goe(startDate)) | ||||
|             .and(order.createdAt.lt(startDate)) | ||||
|  | ||||
|         val where = member.isActive.isTrue | ||||
|             .and(member.role.eq(MemberRole.CREATOR)) | ||||
|             .and(series.isActive.isTrue) | ||||
|             .and(audioContent.isActive.isTrue) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
|             .and(audioContent.limited.isNull) | ||||
|             .and(blockMember.id.isNull) | ||||
|  | ||||
|         return queryFactory | ||||
|             .select( | ||||
|                 QContentCreatorResponse( | ||||
|                     member.id, | ||||
|                     member.nickname, | ||||
|                     member.profileImage.prepend("/").prepend(imageHost) | ||||
|                 ) | ||||
|             ) | ||||
|             .from(seriesContent) | ||||
|             .innerJoin(seriesContent.series, series) | ||||
|             .innerJoin(seriesContent.content, audioContent) | ||||
|             .innerJoin(series.member, member) | ||||
|             .leftJoin(order).on(ordersCondition) | ||||
|             .leftJoin(blockMember).on(blockMemberCondition) | ||||
|             .where(where) | ||||
|             .groupBy(member.id) | ||||
|             .having(series.id.countDistinct().goe(3)) | ||||
|             .orderBy( | ||||
|                 order.can.sum().desc(), | ||||
|                 Expressions.numberTemplate(Double::class.java, "function('rand')").asc() | ||||
|             ) | ||||
|             .offset(0) | ||||
|             .limit(20) | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     fun fetchCreatorSeriesBySales(creatorId: Long, isAdult: Boolean): List<Series> { | ||||
|         var where = member.isActive.isTrue | ||||
|             .and(member.role.eq(MemberRole.CREATOR)) | ||||
|             .and(series.isActive.isTrue) | ||||
|             .and(audioContent.isActive.isTrue) | ||||
|             .and(audioContent.duration.isNotNull) | ||||
|             .and(audioContent.limited.isNull) | ||||
|             .and(order.isActive.isTrue) | ||||
|             .and(member.id.eq(creatorId)) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
|         } | ||||
|  | ||||
|         return queryFactory | ||||
|             .select(series) | ||||
|             .from(seriesContent) | ||||
|             .innerJoin(seriesContent.series, series) | ||||
|             .innerJoin(seriesContent.content, audioContent) | ||||
|             .innerJoin(series.member, member) | ||||
|             .leftJoin(order).on(audioContent.id.eq(order.audioContent.id)) | ||||
|             .where(where) | ||||
|             .groupBy(series.id) | ||||
|             .orderBy( | ||||
|                 order.can.sum().desc(), | ||||
|                 Expressions.numberTemplate(Double::class.java, "function('rand')").asc() | ||||
|             ) | ||||
|             .offset(0) | ||||
|             .limit(10) | ||||
|             .fetch() | ||||
|     } | ||||
| } | ||||
							
								
								
									
										198
									
								
								src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingService.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingService.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | ||||
| package kr.co.vividnext.sodalive.rank | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.main.ContentCreatorResponse | ||||
| import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem | ||||
| import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse | ||||
| import kr.co.vividnext.sodalive.content.series.content.ContentSeriesContentRepository | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.Series | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek | ||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState | ||||
| import kr.co.vividnext.sodalive.explorer.GetExplorerSectionResponse | ||||
| import kr.co.vividnext.sodalive.member.MemberService | ||||
| import org.springframework.beans.factory.annotation.Value | ||||
| import org.springframework.stereotype.Service | ||||
| import java.time.LocalDateTime | ||||
|  | ||||
| @Service | ||||
| class RankingService( | ||||
|     private val repository: RankingRepository, | ||||
|     private val memberService: MemberService, | ||||
|     private val seriesContentRepository: ContentSeriesContentRepository, | ||||
|  | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     private val imageHost: String | ||||
| ) { | ||||
|     fun getCreatorRanking(memberId: Long, rankingDate: String): GetExplorerSectionResponse { | ||||
|         val creatorRankings = repository | ||||
|             .getCreatorRankings() | ||||
|             .filter { !memberService.isBlocked(blockedMemberId = memberId, memberId = it.id!!) } | ||||
|             .map { it.toExplorerSectionCreator(imageHost) } | ||||
|  | ||||
|         return GetExplorerSectionResponse( | ||||
|             title = "인기 크리에이터", | ||||
|             coloredTitle = "인기", | ||||
|             color = "FF5C49", | ||||
|             desc = rankingDate, | ||||
|             creators = creatorRankings | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     fun getContentRanking( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         startDate: LocalDateTime, | ||||
|         endDate: LocalDateTime, | ||||
|         offset: Long = 0, | ||||
|         limit: Long = 12, | ||||
|         sortType: String = "매출", | ||||
|         theme: String = "" | ||||
|     ): List<GetAudioContentRankingItem> { | ||||
|         return repository.getAudioContentRanking( | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult, | ||||
|             startDate = startDate, | ||||
|             endDate = endDate, | ||||
|             offset = offset, | ||||
|             limit = limit, | ||||
|             sortType = sortType, | ||||
|             theme = theme | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     fun getSeriesRanking( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         startDate: LocalDateTime, | ||||
|         endDate: LocalDateTime | ||||
|     ): List<GetSeriesListResponse.SeriesListItem> { | ||||
|         val seriesList = repository.getSeriesRanking(memberId, isAdult, startDate, endDate) | ||||
|         return seriesToSeriesListItem(seriesList = seriesList, isAdult = isAdult) | ||||
|     } | ||||
|  | ||||
|     fun getSeriesAllRankingByGenre( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         genreId: Long | ||||
|     ): List<GetSeriesListResponse.SeriesListItem> { | ||||
|         val seriesList = repository.getSeriesAllRankingByGenre( | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult, | ||||
|             genreId = genreId | ||||
|         ) | ||||
|         return seriesToSeriesListItem(seriesList = seriesList, isAdult = isAdult) | ||||
|     } | ||||
|  | ||||
|     fun getCompleteSeriesRanking( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         startDate: LocalDateTime, | ||||
|         endDate: LocalDateTime, | ||||
|         offset: Long = 0, | ||||
|         limit: Long = 10 | ||||
|     ): List<GetSeriesListResponse.SeriesListItem> { | ||||
|         val seriesList = repository.getCompleteSeriesRanking( | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult, | ||||
|             startDate = startDate, | ||||
|             endDate = endDate, | ||||
|             offset = offset, | ||||
|             limit = limit | ||||
|         ) | ||||
|         return seriesToSeriesListItem(seriesList = seriesList, isAdult = isAdult) | ||||
|     } | ||||
|  | ||||
|     private fun seriesToSeriesListItem( | ||||
|         seriesList: List<Series>, | ||||
|         isAdult: Boolean | ||||
|     ): List<GetSeriesListResponse.SeriesListItem> { | ||||
|         return seriesList | ||||
|             .map { | ||||
|                 GetSeriesListResponse.SeriesListItem( | ||||
|                     seriesId = it.id!!, | ||||
|                     title = it.title, | ||||
|                     coverImage = "$imageHost/${it.coverImage!!}", | ||||
|                     publishedDaysOfWeek = publishedDaysOfWeekText(it.publishedDaysOfWeek), | ||||
|                     isComplete = it.state == SeriesState.COMPLETE, | ||||
|                     creator = GetSeriesListResponse.SeriesListItemCreator( | ||||
|                         creatorId = it.member!!.id!!, | ||||
|                         nickname = it.member!!.nickname, | ||||
|                         profileImage = "$imageHost/${it.member!!.profileImage!!}" | ||||
|                     ) | ||||
|                 ) | ||||
|             } | ||||
|             .map { | ||||
|                 it.numberOfContent = seriesContentRepository.getContentCount( | ||||
|                     seriesId = it.seriesId, | ||||
|                     isAdult = isAdult | ||||
|                 ) | ||||
|  | ||||
|                 it | ||||
|             } | ||||
|             .map { | ||||
|                 val nowDateTime = LocalDateTime.now() | ||||
|  | ||||
|                 it.isNew = seriesContentRepository.isNewContent( | ||||
|                     seriesId = it.seriesId, | ||||
|                     isAdult = isAdult, | ||||
|                     fromDate = nowDateTime.minusDays(7), | ||||
|                     nowDate = nowDateTime | ||||
|                 ) | ||||
|  | ||||
|                 it | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     private fun publishedDaysOfWeekText(publishedDaysOfWeek: Set<SeriesPublishedDaysOfWeek>): String { | ||||
|         val dayOfWeekText = publishedDaysOfWeek.toList().sortedBy { it.ordinal } | ||||
|             .map { | ||||
|                 when (it) { | ||||
|                     SeriesPublishedDaysOfWeek.SUN -> "일" | ||||
|                     SeriesPublishedDaysOfWeek.MON -> "월" | ||||
|                     SeriesPublishedDaysOfWeek.TUE -> "화" | ||||
|                     SeriesPublishedDaysOfWeek.WED -> "수" | ||||
|                     SeriesPublishedDaysOfWeek.THU -> "목" | ||||
|                     SeriesPublishedDaysOfWeek.FRI -> "금" | ||||
|                     SeriesPublishedDaysOfWeek.SAT -> "토" | ||||
|                     SeriesPublishedDaysOfWeek.RANDOM -> "랜덤" | ||||
|                 } | ||||
|             } | ||||
|             .joinToString(", ") { it } | ||||
|  | ||||
|         return if (publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM)) { | ||||
|             dayOfWeekText | ||||
|         } else if (publishedDaysOfWeek.size < 7) { | ||||
|             "매주 $dayOfWeekText" | ||||
|         } else { | ||||
|             "매일" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun fetchCreatorByContentRevenueRankTop20( | ||||
|         memberId: Long, | ||||
|         startDate: LocalDateTime, | ||||
|         endDate: LocalDateTime | ||||
|     ): List<ContentCreatorResponse> { | ||||
|         return repository.fetchCreatorByContentRevenueRankTop20(memberId, startDate, endDate) | ||||
|     } | ||||
|  | ||||
|     fun fetchCreatorContentBySalesTop2(creatorId: Long, isAdult: Boolean): List<GetAudioContentRankingItem> { | ||||
|         return repository.fetchCreatorContentBySalesTop2(creatorId, isAdult) | ||||
|     } | ||||
|  | ||||
|     fun fetchCreatorContentBySalesCountTop2(creatorId: Long, isAdult: Boolean): List<GetAudioContentRankingItem> { | ||||
|         return repository.fetchCreatorContentBySalesCountTop2(creatorId, isAdult) | ||||
|     } | ||||
|  | ||||
|     fun fetchCreatorBySeriesRevenueRankTop20( | ||||
|         memberId: Long, | ||||
|         startDate: LocalDateTime, | ||||
|         endDate: LocalDateTime | ||||
|     ): List<ContentCreatorResponse> { | ||||
|         return repository.fetchCreatorBySeriesRevenueRankTop20(memberId, startDate, endDate) | ||||
|     } | ||||
|  | ||||
|     fun fetchCreatorSeriesBySales(creatorId: Long, isAdult: Boolean): List<GetSeriesListResponse.SeriesListItem> { | ||||
|         val seriesList = repository.fetchCreatorSeriesBySales(creatorId = creatorId, isAdult = isAdult) | ||||
|         return seriesToSeriesListItem(seriesList, isAdult) | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user