diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/AdminRecommendSeriesController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/AdminRecommendSeriesController.kt new file mode 100644 index 0000000..bc747b1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/AdminRecommendSeriesController.kt @@ -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)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/AdminRecommendSeriesRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/AdminRecommendSeriesRepository.kt new file mode 100644 index 0000000..04b47f8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/AdminRecommendSeriesRepository.kt @@ -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, + AdminRecommendSeriesQueryRepository + +interface AdminRecommendSeriesQueryRepository { + fun getRecommendSeriesList(isFree: Boolean): List +} + +class AdminRecommendSeriesQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory, + + @Value("\${cloud.aws.cloud-front.host}") + private val imageHost: String +) : AdminRecommendSeriesQueryRepository { + override fun getRecommendSeriesList(isFree: Boolean): List { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/AdminRecommendSeriesService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/AdminRecommendSeriesService.kt new file mode 100644 index 0000000..cf697e4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/AdminRecommendSeriesService.kt @@ -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 { + 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) { + for (index in ids.indices) { + val recommendSeries = repository.findByIdOrNull(ids[index]) + + if (recommendSeries != null) { + recommendSeries.orders = index + 1 + } + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/CreateRecommendSeriesRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/CreateRecommendSeriesRequest.kt new file mode 100644 index 0000000..7bb67be --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/CreateRecommendSeriesRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.admin.content.series.recommend + +data class CreateRecommendSeriesRequest( + val seriesId: Long, + val isFree: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/GetAdminRecommendSeriesListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/GetAdminRecommendSeriesListResponse.kt new file mode 100644 index 0000000..ec9b039 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/GetAdminRecommendSeriesListResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/UpdateRecommendSeriesOrdersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/UpdateRecommendSeriesOrdersRequest.kt new file mode 100644 index 0000000..90038e0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/UpdateRecommendSeriesOrdersRequest.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.admin.content.series.recommend + +data class UpdateRecommendSeriesOrdersRequest( + val ids: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/UpdateRecommendSeriesRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/UpdateRecommendSeriesRequest.kt new file mode 100644 index 0000000..d820c74 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/recommend/UpdateRecommendSeriesRequest.kt @@ -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? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt index 34261ea..fa0ea92 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/audition/vote/AuditionVoteService.kt @@ -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) { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt index f1ac4e8..f855b57 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -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 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 fun totalCountByTheme( @@ -98,13 +99,12 @@ interface AudioContentQueryRepository { fun getNewContentUploadCreatorList( cloudfrontHost: String, isAdult: Boolean = false - ): List + ): List fun getAudioContentMainBannerList(isAdult: Boolean): List fun getAudioContentCurations(isAdult: Boolean): List fun findAudioContentByCurationId( curationId: Long, - cloudfrontHost: String, isAdult: Boolean, contentType: ContentType ): List @@ -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 { + 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 { + ): List { 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 { @@ -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 diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt index 3e32d6d..b72b829 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainService.kt @@ -39,7 +39,6 @@ class AudioContentMainService( pageable: Pageable ): List { 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 { + fun getNewContentUploadCreatorList(memberId: Long, isAdult: Boolean): List { 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 ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetNewContentUploadCreator.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/ContentCreatorResponse.kt similarity index 83% rename from src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetNewContentUploadCreator.kt rename to src/main/kotlin/kr/co/vividnext/sodalive/content/main/ContentCreatorResponse.kt index a53d1d3..c6ace0c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/GetNewContentUploadCreator.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/ContentCreatorResponse.kt @@ -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 diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerRepository.kt new file mode 100644 index 0000000..0fd76c6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerRepository.kt @@ -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, AudioContentBannerQueryRepository + +interface AudioContentBannerQueryRepository { + fun getAudioContentMainBannerList(tabId: Long, isAdult: Boolean): List +} + +class AudioContentBannerQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : AudioContentBannerQueryRepository { + override fun getAudioContentMainBannerList(tabId: Long, isAdult: Boolean): List { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerService.kt new file mode 100644 index 0000000..022a888 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerService.kt @@ -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 { + 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 + ) + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/AudioContentCurationQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/AudioContentCurationQueryRepository.kt index 916d860..28c6985 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/AudioContentCurationQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/curation/AudioContentCurationQueryRepository.kt @@ -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 { + 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 { + 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() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/GetContentCurationResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/GetContentCurationResponse.kt new file mode 100644 index 0000000..b020326 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/GetContentCurationResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/GetRecommendSeriesListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/GetRecommendSeriesListResponse.kt new file mode 100644 index 0000000..a84a20d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/GetRecommendSeriesListResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/RecommendSeries.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/RecommendSeries.kt new file mode 100644 index 0000000..9d0290d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/RecommendSeries.kt @@ -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 +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/RecommendSeriesRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/RecommendSeriesRepository.kt new file mode 100644 index 0000000..b17a583 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/RecommendSeriesRepository.kt @@ -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 { + 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 { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmController.kt new file mode 100644 index 0000000..68bae3c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmController.kt @@ -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)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmService.kt new file mode 100644 index 0000000..81128fe --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/AudioContentMainTabAlarmService.kt @@ -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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/GetContentMainTabAlarmResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/GetContentMainTabAlarmResponse.kt new file mode 100644 index 0000000..46d61de --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/alarm/GetContentMainTabAlarmResponse.kt @@ -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, + val alarmThemeList: List, + val newAlarmContentList: List, + val rankAlarmContentList: List, + val eventBannerList: GetEventResponse, + val curationList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrController.kt new file mode 100644 index 0000000..ad35a6d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrController.kt @@ -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)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrService.kt new file mode 100644 index 0000000..9748778 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/AudioContentMainTabAsmrService.kt @@ -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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/GetContentMainTabAsmrResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/GetContentMainTabAsmrResponse.kt new file mode 100644 index 0000000..5b406e1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/asmr/GetContentMainTabAsmrResponse.kt @@ -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, + val newAsmrContentList: List, + val rankAsmrContentList: List, + val eventBannerList: GetEventResponse, + val curationList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentController.kt new file mode 100644 index 0000000..80762a1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentController.kt @@ -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)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentService.kt new file mode 100644 index 0000000..6807e14 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/AudioContentMainTabContentService.kt @@ -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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/GetContentMainTabContentResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/GetContentMainTabContentResponse.kt new file mode 100644 index 0000000..721e960 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/GetContentMainTabContentResponse.kt @@ -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, + val contentThemeList: List, + val newContentList: List, + val rankSortTypeList: List, + val rankContentList: List, + val contentRankCreatorList: List, + val salesRankContentList: List, + val salesCountRankContentList: List, + val eventBannerList: GetEventResponse +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/free/AudioContentMainTabFreeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/free/AudioContentMainTabFreeController.kt new file mode 100644 index 0000000..e588782 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/free/AudioContentMainTabFreeController.kt @@ -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)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/free/AudioContentMainTabFreeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/free/AudioContentMainTabFreeService.kt new file mode 100644 index 0000000..b64a2ab --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/free/AudioContentMainTabFreeService.kt @@ -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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/free/GetContentMainTabFreeResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/free/GetContentMainTabFreeResponse.kt new file mode 100644 index 0000000..aed9911 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/free/GetContentMainTabFreeResponse.kt @@ -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, + val introduceCreator: GetContentCurationResponse?, + val recommendSeriesList: List, + val themeList: List, + val newFreeContentList: List, + val curationList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeController.kt new file mode 100644 index 0000000..601f82d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeController.kt @@ -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)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt new file mode 100644 index 0000000..8532a33 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt @@ -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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/GetContentMainTabHomeResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/GetContentMainTabHomeResponse.kt new file mode 100644 index 0000000..c104a77 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/GetContentMainTabHomeResponse.kt @@ -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, + val rankCreatorList: GetExplorerSectionResponse, + val rankSeriesList: List, + val rankSortTypeList: List, + val rankContentList: List, + val eventBannerList: GetEventResponse, + val contentRankCreatorList: List, + val salesRankContentList: List, + val salesCountRankContentList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayController.kt new file mode 100644 index 0000000..c9b3739 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayController.kt @@ -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)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayService.kt new file mode 100644 index 0000000..560ca76 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/AudioContentMainTabLiveReplayService.kt @@ -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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/GetContentMainTabLiveReplayResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/GetContentMainTabLiveReplayResponse.kt new file mode 100644 index 0000000..eea8309 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/replay/GetContentMainTabLiveReplayResponse.kt @@ -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, + val newLiveReplayContentList: List, + val rankLiveReplayContentList: List, + val eventBannerList: GetEventResponse, + val curationList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesController.kt new file mode 100644 index 0000000..1e27aa6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesController.kt @@ -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)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesService.kt new file mode 100644 index 0000000..681dd5f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/AudioContentMainTabSeriesService.kt @@ -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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/GetContentMainTabSeriesResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/GetContentMainTabSeriesResponse.kt new file mode 100644 index 0000000..d7155b0 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/GetContentMainTabSeriesResponse.kt @@ -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, + val originalAudioDrama: List, + val rankSeriesList: List, + val genreList: List, + val recommendSeriesList: List, + val newSeriesList: List, + val rankCompleteSeriesList: List, + val seriesRankCreatorList: List, + val salesRankContentList: List, + val eventBannerList: GetEventResponse, + val curationList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/GetSeriesCurationResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/GetSeriesCurationResponse.kt new file mode 100644 index 0000000..bb24415 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/series/GetSeriesCurationResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt index 10b8f25..e87f059 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt @@ -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, ContentSeriesQueryRepository @@ -29,6 +35,9 @@ interface ContentSeriesQueryRepository { fun getKeywordList(seriesId: Long): List fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse fun getRecommendSeriesList(isAuth: Boolean, contentType: ContentType, limit: Long): List + fun getOriginalAudioDramaList(isAdult: Boolean, offset: Long = 0, limit: Long = 20): List + fun getGenreList(isAdult: Boolean): List + fun findByCurationId(curationId: Long, memberId: Long, isAdult: Boolean): List } class ContentSeriesQueryRepositoryImpl( @@ -133,4 +142,61 @@ class ContentSeriesQueryRepositoryImpl( .limit(limit) .fetch() } + + override fun getOriginalAudioDramaList(isAdult: Boolean, offset: Long, limit: Long): List { + 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 { + 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 { + 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() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt index 2a60ae3..1e95021 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesService.kt @@ -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 { + val originalAudioDramaList = repository.getOriginalAudioDramaList(isAdult, offset, limit) + return seriesToSeriesListItem(originalAudioDramaList, isAdult) + } + + fun getGenreList(isAdult: Boolean): List { + 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 { + val seriesList = repository.findByCurationId(curationId = curationId, memberId = memberId, isAdult = isAdult) + return seriesToSeriesListItem(seriesList, isAdult) + } + private fun seriesToSeriesListItem( seriesList: List, isAdult: Boolean diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesGenreListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesGenreListResponse.kt new file mode 100644 index 0000000..30aa078 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/series/GetSeriesGenreListResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeService.kt index 5a3c6a9..b0ad97a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/theme/AudioContentThemeService.kt @@ -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 { @@ -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, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt index e0bc549..ac823f2 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthController.kt @@ -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!!)) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthResponse.kt new file mode 100644 index 0000000..b061dc6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthResponse.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.member.auth + +data class AuthResponse(val gender: Int) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt index 9d24456..53ce8de 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/auth/AuthService.kt @@ -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일 이전 출생자만 본인인증이 가능합니다.") } } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/GetNoticeResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/GetNoticeResponse.kt index aa35bbf..246846e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/notice/GetNoticeResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/GetNoticeResponse.kt @@ -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 @@ -11,3 +13,8 @@ data class NoticeItem( val content: String, val date: String ) + +data class NoticeTitleItem @QueryProjection constructor( + val id: Long, + val title: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeService.kt index 10d28c2..f76e57f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceNoticeService.kt @@ -63,4 +63,8 @@ class ServiceNoticeService(private val repository: ServiceServiceNoticeRepositor return GetNoticeResponse(totalCount, noticeList) } + + fun getLatestNotice(): NoticeTitleItem? { + return repository.getLatestNotice() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceServiceNoticeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceServiceNoticeRepository.kt index 90d5a47..d5772f5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceServiceNoticeRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/notice/ServiceServiceNoticeRepository.kt @@ -12,6 +12,7 @@ interface ServiceServiceNoticeRepository : JpaRepository, S interface ServiceNoticeQueryRepository { fun getNoticeTotalCount(): Int fun getNoticeList(pageable: Pageable): List + 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() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingRepository.kt new file mode 100644 index 0000000..2250eee --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingRepository.kt @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingService.kt new file mode 100644 index 0000000..a3504fc --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingService.kt @@ -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 { + 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 { + val seriesList = repository.getSeriesRanking(memberId, isAdult, startDate, endDate) + return seriesToSeriesListItem(seriesList = seriesList, isAdult = isAdult) + } + + fun getSeriesAllRankingByGenre( + memberId: Long, + isAdult: Boolean, + genreId: Long + ): List { + 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 { + 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, + isAdult: Boolean + ): List { + 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): 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 { + return repository.fetchCreatorByContentRevenueRankTop20(memberId, startDate, endDate) + } + + fun fetchCreatorContentBySalesTop2(creatorId: Long, isAdult: Boolean): List { + return repository.fetchCreatorContentBySalesTop2(creatorId, isAdult) + } + + fun fetchCreatorContentBySalesCountTop2(creatorId: Long, isAdult: Boolean): List { + return repository.fetchCreatorContentBySalesCountTop2(creatorId, isAdult) + } + + fun fetchCreatorBySeriesRevenueRankTop20( + memberId: Long, + startDate: LocalDateTime, + endDate: LocalDateTime + ): List { + return repository.fetchCreatorBySeriesRevenueRankTop20(memberId, startDate, endDate) + } + + fun fetchCreatorSeriesBySales(creatorId: Long, isAdult: Boolean): List { + val seriesList = repository.fetchCreatorSeriesBySales(creatorId = creatorId, isAdult = isAdult) + return seriesToSeriesListItem(seriesList, isAdult) + } +}