From 04eb416a73fbe9ac0d3ffd1f5209ada7f4ad4432 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 6 Feb 2025 19:06:27 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20-=20=ED=99=88=20=ED=83=AD=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/AudioContentRepository.kt | 8 +- .../content/main/AudioContentMainService.kt | 2 +- ...adCreator.kt => ContentCreatorResponse.kt} | 2 +- .../banner/AudioContentBannerRepository.kt | 41 +++ .../main/banner/AudioContentBannerService.kt | 67 ++++ .../home/AudioContentMainTabHomeController.kt | 22 ++ .../home/AudioContentMainTabHomeService.kt | 109 ++++++ .../tab/home/GetContentMainTabHomeResponse.kt | 23 ++ .../sodalive/notice/GetNoticeResponse.kt | 7 + .../sodalive/notice/ServiceNoticeService.kt | 4 + .../notice/ServiceServiceNoticeRepository.kt | 15 + .../sodalive/rank/RankingRepository.kt | 318 ++++++++++++++++++ .../vividnext/sodalive/rank/RankingService.kt | 151 +++++++++ 13 files changed, 763 insertions(+), 6 deletions(-) rename src/main/kotlin/kr/co/vividnext/sodalive/content/main/{GetNewContentUploadCreator.kt => ContentCreatorResponse.kt} (83%) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/GetContentMainTabHomeResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingService.kt 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..d758d01 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 @@ -98,7 +98,7 @@ interface AudioContentQueryRepository { fun getNewContentUploadCreatorList( cloudfrontHost: String, isAdult: Boolean = false - ): List + ): List fun getAudioContentMainBannerList(isAdult: Boolean): List fun getAudioContentCurations(isAdult: Boolean): List @@ -533,7 +533,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 +552,7 @@ class AudioContentQueryRepositoryImpl( .limit(20) .fetch() .map { - GetNewContentUploadCreator( + ContentCreatorResponse( it.id!!, it.nickname, creatorProfileImageUrl = if (it.profileImage != null) { 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..2963ec5 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 @@ -80,7 +80,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 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/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..f4693f7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/home/AudioContentMainTabHomeService.kt @@ -0,0 +1,109 @@ +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(7) + + 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.minusDays(1) + ) + + // 인기 콘텐츠 + val rankContentList = rankingService.getContentRanking( + memberId = member.id!!, + isAdult = member.auth != null, + startDate = startDate.minusDays(1), + endDate = endDate.minusDays(1) + ) + + // 이벤트 배너 + 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.minusDays(1) + ) + + val salesRankContentList = rankingService.fetchCreatorContentBySalesTop2( + creatorId = contentRankCreatorList[0].creatorId, + isAdult = member.auth != null + ) + + val salesCountRankContentList = rankingService.fetchCreatorContentBySalesCountTop2( + creatorId = contentRankCreatorList[0].creatorId, + isAdult = member.auth != null + ) + + 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/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..67d17a4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingRepository.kt @@ -0,0 +1,318 @@ +package kr.co.vividnext.sodalive.rank + +import com.querydsl.jpa.impl.JPAQueryFactory +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.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 + ): 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) + } + + 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) + .innerJoin(order).on(audioContent.id.eq(order.audioContent.id)) + .leftJoin(blockMember).on(blockMemberCondition) + .where(where) + .groupBy(series.id) + .orderBy(order.can.sum().desc(), series.createdAt.asc()) + .offset(0) + .limit(10) + .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) + .and(order.can.sum().gt(0)) + ) + .orderBy(order.can.sum().desc()) + .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() + } +} 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..662a668 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingService.kt @@ -0,0 +1,151 @@ +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 = "매출" + ): List { + return repository.getAudioContentRanking( + memberId = memberId, + isAdult = isAdult, + startDate = startDate, + endDate = endDate, + offset = offset, + limit = limit, + sortType = sortType + ) + } + + 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) + } + + 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) + } +}