test #327
| @@ -0,0 +1,26 @@ | |||||||
|  | package kr.co.vividnext.sodalive.api.home | ||||||
|  |  | ||||||
|  | import kr.co.vividnext.sodalive.audition.GetAuditionListItem | ||||||
|  | import kr.co.vividnext.sodalive.content.AudioContentMainItem | ||||||
|  | import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem | ||||||
|  | import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse | ||||||
|  | import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse | ||||||
|  | import kr.co.vividnext.sodalive.event.GetEventResponse | ||||||
|  | import kr.co.vividnext.sodalive.explorer.GetExplorerSectionCreatorResponse | ||||||
|  | import kr.co.vividnext.sodalive.live.room.GetRoomListResponse | ||||||
|  | import kr.co.vividnext.sodalive.query.recommend.RecommendChannelResponse | ||||||
|  |  | ||||||
|  | data class GetHomeResponse( | ||||||
|  |     val liveList: List<GetRoomListResponse>, | ||||||
|  |     val creatorRanking: List<GetExplorerSectionCreatorResponse>, | ||||||
|  |     val latestContentThemeList: List<String>, | ||||||
|  |     val latestContentList: List<AudioContentMainItem>, | ||||||
|  |     val eventBannerList: GetEventResponse, | ||||||
|  |     val originalAudioDramaList: List<GetSeriesListResponse.SeriesListItem>, | ||||||
|  |     val auditionList: List<GetAuditionListItem>, | ||||||
|  |     val dayOfWeekSeriesList: List<GetSeriesListResponse.SeriesListItem>, | ||||||
|  |     val contentRanking: List<GetAudioContentRankingItem>, | ||||||
|  |     val recommendChannelList: List<RecommendChannelResponse>, | ||||||
|  |     val freeContentList: List<AudioContentMainItem>, | ||||||
|  |     val curationList: List<GetContentCurationResponse> | ||||||
|  | ) | ||||||
| @@ -0,0 +1,66 @@ | |||||||
|  | package kr.co.vividnext.sodalive.api.home | ||||||
|  |  | ||||||
|  | import kr.co.vividnext.sodalive.common.ApiResponse | ||||||
|  | import kr.co.vividnext.sodalive.content.ContentType | ||||||
|  | import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek | ||||||
|  | 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.RequestParam | ||||||
|  | import org.springframework.web.bind.annotation.RestController | ||||||
|  |  | ||||||
|  | @RestController | ||||||
|  | @RequestMapping("/api/home") | ||||||
|  | class HomeController(private val service: HomeService) { | ||||||
|  |     @GetMapping | ||||||
|  |     fun fetchData( | ||||||
|  |         @RequestParam timezone: String, | ||||||
|  |         @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, | ||||||
|  |         @RequestParam("contentType", required = false) contentType: ContentType? = null, | ||||||
|  |         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||||
|  |     ) = run { | ||||||
|  |         ApiResponse.ok( | ||||||
|  |             service.fetchData( | ||||||
|  |                 timezone = timezone, | ||||||
|  |                 isAdultContentVisible = isAdultContentVisible ?: true, | ||||||
|  |                 contentType = contentType ?: ContentType.ALL, | ||||||
|  |                 member | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @GetMapping("/latest-content") | ||||||
|  |     fun getLatestContentByTheme( | ||||||
|  |         @RequestParam("theme") theme: String, | ||||||
|  |         @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, | ||||||
|  |         @RequestParam("contentType", required = false) contentType: ContentType? = null, | ||||||
|  |         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||||
|  |     ) = run { | ||||||
|  |         ApiResponse.ok( | ||||||
|  |             service.getLatestContentByTheme( | ||||||
|  |                 theme = theme, | ||||||
|  |                 isAdultContentVisible = isAdultContentVisible ?: true, | ||||||
|  |                 contentType = contentType ?: ContentType.ALL, | ||||||
|  |                 member | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @GetMapping("/day-of-week-series") | ||||||
|  |     fun getDayOfWeekSeriesList( | ||||||
|  |         @RequestParam("dayOfWeek") dayOfWeek: SeriesPublishedDaysOfWeek, | ||||||
|  |         @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, | ||||||
|  |         @RequestParam("contentType", required = false) contentType: ContentType? = null, | ||||||
|  |         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||||
|  |     ) = run { | ||||||
|  |         ApiResponse.ok( | ||||||
|  |             service.getDayOfWeekSeriesList( | ||||||
|  |                 dayOfWeek = dayOfWeek, | ||||||
|  |                 isAdultContentVisible = isAdultContentVisible ?: true, | ||||||
|  |                 contentType = contentType ?: ContentType.ALL, | ||||||
|  |                 member | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										243
									
								
								src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,243 @@ | |||||||
|  | package kr.co.vividnext.sodalive.api.home | ||||||
|  |  | ||||||
|  | import kr.co.vividnext.sodalive.audition.AuditionService | ||||||
|  | import kr.co.vividnext.sodalive.content.AudioContentMainItem | ||||||
|  | import kr.co.vividnext.sodalive.content.AudioContentService | ||||||
|  | import kr.co.vividnext.sodalive.content.ContentType | ||||||
|  | import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationService | ||||||
|  | import kr.co.vividnext.sodalive.content.series.ContentSeriesService | ||||||
|  | import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse | ||||||
|  | import kr.co.vividnext.sodalive.content.theme.AudioContentThemeService | ||||||
|  | import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek | ||||||
|  | import kr.co.vividnext.sodalive.event.EventService | ||||||
|  | import kr.co.vividnext.sodalive.live.room.LiveRoomService | ||||||
|  | import kr.co.vividnext.sodalive.live.room.LiveRoomStatus | ||||||
|  | import kr.co.vividnext.sodalive.member.Member | ||||||
|  | import kr.co.vividnext.sodalive.member.MemberService | ||||||
|  | import kr.co.vividnext.sodalive.query.recommend.RecommendChannelQueryService | ||||||
|  | import kr.co.vividnext.sodalive.rank.RankingRepository | ||||||
|  | import kr.co.vividnext.sodalive.rank.RankingService | ||||||
|  | import org.springframework.beans.factory.annotation.Value | ||||||
|  | import org.springframework.data.domain.Pageable | ||||||
|  | import org.springframework.stereotype.Service | ||||||
|  | import java.time.DayOfWeek | ||||||
|  | import java.time.LocalDateTime | ||||||
|  | import java.time.ZoneId | ||||||
|  | import java.time.temporal.TemporalAdjusters | ||||||
|  |  | ||||||
|  | @Service | ||||||
|  | class HomeService( | ||||||
|  |     private val eventService: EventService, | ||||||
|  |     private val memberService: MemberService, | ||||||
|  |     private val liveRoomService: LiveRoomService, | ||||||
|  |     private val auditionService: AuditionService, | ||||||
|  |     private val seriesService: ContentSeriesService, | ||||||
|  |     private val contentService: AudioContentService, | ||||||
|  |     private val curationService: AudioContentCurationService, | ||||||
|  |     private val contentThemeService: AudioContentThemeService, | ||||||
|  |     private val recommendChannelService: RecommendChannelQueryService, | ||||||
|  |  | ||||||
|  |     private val rankingService: RankingService, | ||||||
|  |     private val rankingRepository: RankingRepository, | ||||||
|  |  | ||||||
|  |     @Value("\${cloud.aws.cloud-front.host}") | ||||||
|  |     private val imageHost: String | ||||||
|  | ) { | ||||||
|  |     fun fetchData( | ||||||
|  |         timezone: String, | ||||||
|  |         isAdultContentVisible: Boolean, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         member: Member? | ||||||
|  |     ): GetHomeResponse { | ||||||
|  |         val memberId = member?.id | ||||||
|  |         val isAdult = member?.auth != null && isAdultContentVisible | ||||||
|  |  | ||||||
|  |         val liveList = liveRoomService.getRoomList( | ||||||
|  |             dateString = null, | ||||||
|  |             status = LiveRoomStatus.NOW, | ||||||
|  |             isAdultContentVisible = isAdultContentVisible, | ||||||
|  |             pageable = Pageable.ofSize(10), | ||||||
|  |             member = member, | ||||||
|  |             timezone = timezone | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         val creatorRanking = rankingRepository | ||||||
|  |             .getCreatorRankings() | ||||||
|  |             .filter { | ||||||
|  |                 if (memberId != null) { | ||||||
|  |                     !memberService.isBlocked(blockedMemberId = memberId, memberId = it.id!!) | ||||||
|  |                 } else { | ||||||
|  |                     true | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .map { it.toExplorerSectionCreator(imageHost) } | ||||||
|  |  | ||||||
|  |         val latestContentThemeList = contentThemeService.getActiveThemeOfContent( | ||||||
|  |             isAdult = isAdult, | ||||||
|  |             contentType = contentType | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         val latestContentList = contentService.getLatestContentByTheme( | ||||||
|  |             theme = latestContentThemeList, | ||||||
|  |             contentType = contentType, | ||||||
|  |             isFree = false, | ||||||
|  |             isAdult = isAdult | ||||||
|  |         ).filter { | ||||||
|  |             if (memberId != null) { | ||||||
|  |                 !memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId) | ||||||
|  |             } else { | ||||||
|  |                 true | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val eventBannerList = eventService.getEventList(isAdult = isAdult) | ||||||
|  |  | ||||||
|  |         val originalAudioDramaList = seriesService.getOriginalAudioDramaList( | ||||||
|  |             isAdult = isAdult, | ||||||
|  |             contentType = contentType | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         val auditionList = auditionService.getInProgressAuditionList(isAdult = isAdult) | ||||||
|  |  | ||||||
|  |         val dayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList( | ||||||
|  |             memberId = memberId, | ||||||
|  |             isAdult = isAdult, | ||||||
|  |             contentType = contentType, | ||||||
|  |             dayOfWeek = getDayOfWeekByTimezone(timezone) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         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 contentRanking = rankingService.getContentRanking( | ||||||
|  |             memberId = memberId, | ||||||
|  |             isAdult = isAdult, | ||||||
|  |             contentType = contentType, | ||||||
|  |             startDate = startDate.minusDays(1), | ||||||
|  |             endDate = endDate, | ||||||
|  |             sortType = "매출" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         // TODO 오디오 북 | ||||||
|  |  | ||||||
|  |         val recommendChannelList = recommendChannelService.getRecommendChannel( | ||||||
|  |             memberId = memberId, | ||||||
|  |             isAdult = isAdult, | ||||||
|  |             contentType = contentType | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         val freeContentList = contentService.getLatestContentByTheme( | ||||||
|  |             theme = contentThemeService.getActiveThemeOfContent( | ||||||
|  |                 isAdult = isAdult, | ||||||
|  |                 isFree = true, | ||||||
|  |                 contentType = contentType | ||||||
|  |             ), | ||||||
|  |             contentType = contentType, | ||||||
|  |             isFree = true, | ||||||
|  |             isAdult = isAdult | ||||||
|  |         ).filter { | ||||||
|  |             if (memberId != null) { | ||||||
|  |                 !memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId) | ||||||
|  |             } else { | ||||||
|  |                 true | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val curationList = curationService.getContentCurationList( | ||||||
|  |             tabId = 3L, // 기존에 사용하던 단편 탭의 큐레이션을 사용 | ||||||
|  |             isAdult = isAdult, | ||||||
|  |             contentType = contentType, | ||||||
|  |             memberId = memberId | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return GetHomeResponse( | ||||||
|  |             liveList = liveList, | ||||||
|  |             creatorRanking = creatorRanking, | ||||||
|  |             latestContentThemeList = latestContentThemeList, | ||||||
|  |             latestContentList = latestContentList, | ||||||
|  |             eventBannerList = eventBannerList, | ||||||
|  |             originalAudioDramaList = originalAudioDramaList, | ||||||
|  |             auditionList = auditionList, | ||||||
|  |             dayOfWeekSeriesList = dayOfWeekSeriesList, | ||||||
|  |             contentRanking = contentRanking, | ||||||
|  |             recommendChannelList = recommendChannelList, | ||||||
|  |             freeContentList = freeContentList, | ||||||
|  |             curationList = curationList | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getLatestContentByTheme( | ||||||
|  |         theme: String, | ||||||
|  |         isAdultContentVisible: Boolean, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         member: Member? | ||||||
|  |     ): List<AudioContentMainItem> { | ||||||
|  |         val memberId = member?.id | ||||||
|  |         val isAdult = member?.auth != null && isAdultContentVisible | ||||||
|  |  | ||||||
|  |         val themeList = if (theme.isBlank()) { | ||||||
|  |             contentThemeService.getActiveThemeOfContent( | ||||||
|  |                 isAdult = isAdult, | ||||||
|  |                 isFree = true, | ||||||
|  |                 contentType = contentType | ||||||
|  |             ) | ||||||
|  |         } else { | ||||||
|  |             listOf(theme) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return contentService.getLatestContentByTheme( | ||||||
|  |             theme = themeList, | ||||||
|  |             contentType = contentType, | ||||||
|  |             isFree = false, | ||||||
|  |             isAdult = isAdult | ||||||
|  |         ).filter { | ||||||
|  |             if (memberId != null) { | ||||||
|  |                 !memberService.isBlocked(blockedMemberId = memberId, memberId = it.creatorId) | ||||||
|  |             } else { | ||||||
|  |                 true | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getDayOfWeekSeriesList( | ||||||
|  |         dayOfWeek: SeriesPublishedDaysOfWeek, | ||||||
|  |         isAdultContentVisible: Boolean, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         member: Member? | ||||||
|  |     ): List<GetSeriesListResponse.SeriesListItem> { | ||||||
|  |         val memberId = member?.id | ||||||
|  |         val isAdult = member?.auth != null && isAdultContentVisible | ||||||
|  |  | ||||||
|  |         return seriesService.getDayOfWeekSeriesList( | ||||||
|  |             memberId = memberId, | ||||||
|  |             isAdult = isAdult, | ||||||
|  |             contentType = contentType, | ||||||
|  |             dayOfWeek = dayOfWeek | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun getDayOfWeekByTimezone(timezone: String): SeriesPublishedDaysOfWeek { | ||||||
|  |         val systemTime = LocalDateTime.now() | ||||||
|  |         val zoneId = ZoneId.of(timezone) | ||||||
|  |         val zonedDateTime = systemTime.atZone(ZoneId.systemDefault()).withZoneSameInstant(zoneId) | ||||||
|  |  | ||||||
|  |         val dayToSeriesPublishedDaysOfWeek = mapOf( | ||||||
|  |             DayOfWeek.MONDAY to SeriesPublishedDaysOfWeek.MON, | ||||||
|  |             DayOfWeek.TUESDAY to SeriesPublishedDaysOfWeek.TUE, | ||||||
|  |             DayOfWeek.WEDNESDAY to SeriesPublishedDaysOfWeek.WED, | ||||||
|  |             DayOfWeek.THURSDAY to SeriesPublishedDaysOfWeek.THU, | ||||||
|  |             DayOfWeek.FRIDAY to SeriesPublishedDaysOfWeek.FRI, | ||||||
|  |             DayOfWeek.SATURDAY to SeriesPublishedDaysOfWeek.SAT, | ||||||
|  |             DayOfWeek.SUNDAY to SeriesPublishedDaysOfWeek.SUN | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return dayToSeriesPublishedDaysOfWeek[zonedDateTime.dayOfWeek] ?: SeriesPublishedDaysOfWeek.RANDOM | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -12,6 +12,7 @@ interface AuditionQueryRepository { | |||||||
|     fun getCompletedAuditionCount(isAdult: Boolean): Int |     fun getCompletedAuditionCount(isAdult: Boolean): Int | ||||||
|     fun getAuditionList(offset: Long, limit: Long, isAdult: Boolean): List<GetAuditionListItem> |     fun getAuditionList(offset: Long, limit: Long, isAdult: Boolean): List<GetAuditionListItem> | ||||||
|     fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData |     fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData | ||||||
|  |     fun getInProgressAuditionList(isAdult: Boolean): List<GetAuditionListItem> | ||||||
| } | } | ||||||
|  |  | ||||||
| class AuditionQueryRepositoryImpl( | class AuditionQueryRepositoryImpl( | ||||||
| @@ -94,4 +95,27 @@ class AuditionQueryRepositoryImpl( | |||||||
|             .where(audition.id.eq(auditionId)) |             .where(audition.id.eq(auditionId)) | ||||||
|             .fetchFirst() |             .fetchFirst() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override fun getInProgressAuditionList(isAdult: Boolean): List<GetAuditionListItem> { | ||||||
|  |         var where = audition.isActive.isTrue | ||||||
|  |             .and(audition.status.eq(AuditionStatus.IN_PROGRESS)) | ||||||
|  |  | ||||||
|  |         if (!isAdult) { | ||||||
|  |             where = where.and(audition.isAdult.isFalse) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return queryFactory | ||||||
|  |             .select( | ||||||
|  |                 QGetAuditionListItem( | ||||||
|  |                     audition.id, | ||||||
|  |                     audition.title, | ||||||
|  |                     audition.imagePath.prepend("/").prepend(cloudFrontHost), | ||||||
|  |                     audition.status.eq(AuditionStatus.COMPLETED) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             .from(audition) | ||||||
|  |             .where(where) | ||||||
|  |             .orderBy(audition.status.desc()) | ||||||
|  |             .fetch() | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -28,4 +28,8 @@ class AuditionService( | |||||||
|             roleList = roleList |             roleList = roleList | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fun getInProgressAuditionList(isAdult: Boolean): List<GetAuditionListItem> { | ||||||
|  |         return repository.getInProgressAuditionList(isAdult) | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,13 @@ | |||||||
|  | package kr.co.vividnext.sodalive.content | ||||||
|  |  | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty | ||||||
|  | import com.querydsl.core.annotations.QueryProjection | ||||||
|  |  | ||||||
|  | data class AudioContentMainItem @QueryProjection constructor( | ||||||
|  |     @JsonProperty("contentId") val contentId: Long, | ||||||
|  |     @JsonProperty("creatorId") val creatorId: Long, | ||||||
|  |     @JsonProperty("title") val title: String, | ||||||
|  |     @JsonProperty("coverImageUrl") val coverImageUrl: String, | ||||||
|  |     @JsonProperty("creatorNickname") val creatorNickname: String, | ||||||
|  |     @JsonProperty("isPointAvailable") val isPointAvailable: Boolean | ||||||
|  | ) | ||||||
| @@ -176,6 +176,23 @@ interface AudioContentQueryRepository { | |||||||
|     fun findContentHashTagByContentIdAndIsActive(contentId: Long, isActive: Boolean): List<AudioContentHashTag> |     fun findContentHashTagByContentIdAndIsActive(contentId: Long, isActive: Boolean): List<AudioContentHashTag> | ||||||
|  |  | ||||||
|     fun findContentIdAndHashTagId(contentId: Long, hashTagId: Int): AudioContentHashTag? |     fun findContentIdAndHashTagId(contentId: Long, hashTagId: Int): AudioContentHashTag? | ||||||
|  |  | ||||||
|  |     fun getLatestContentByTheme( | ||||||
|  |         theme: List<String>, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         offset: Long, | ||||||
|  |         limit: Long, | ||||||
|  |         isFree: Boolean, | ||||||
|  |         isAdult: Boolean | ||||||
|  |     ): List<AudioContentMainItem> | ||||||
|  |  | ||||||
|  |     fun findContentByCurationId( | ||||||
|  |         curationId: Long, | ||||||
|  |         isAdult: Boolean, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         offset: Long = 0, | ||||||
|  |         limit: Long = 20 | ||||||
|  |     ): List<GetAudioContentMainItem> | ||||||
| } | } | ||||||
|  |  | ||||||
| @Repository | @Repository | ||||||
| @@ -1281,4 +1298,126 @@ class AudioContentQueryRepositoryImpl( | |||||||
|             .orderBy(audioContentHashTag.id.asc()) |             .orderBy(audioContentHashTag.id.asc()) | ||||||
|             .fetchFirst() |             .fetchFirst() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override fun getLatestContentByTheme( | ||||||
|  |         theme: List<String>, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         offset: Long, | ||||||
|  |         limit: Long, | ||||||
|  |         isFree: Boolean, | ||||||
|  |         isAdult: Boolean | ||||||
|  |     ): List<AudioContentMainItem> { | ||||||
|  |         var where = audioContent.isActive.isTrue | ||||||
|  |             .and(audioContent.duration.isNotNull) | ||||||
|  |             .and( | ||||||
|  |                 audioContent.releaseDate.isNull | ||||||
|  |                     .or(audioContent.releaseDate.loe(LocalDateTime.now())) | ||||||
|  |             ) | ||||||
|  |             .and(blockMember.id.isNull) | ||||||
|  |  | ||||||
|  |         if (!isAdult) { | ||||||
|  |             where = where.and(audioContent.isAdult.isFalse) | ||||||
|  |         } else { | ||||||
|  |             if (contentType != ContentType.ALL) { | ||||||
|  |                 where = where.and( | ||||||
|  |                     audioContent.member.isNull.or( | ||||||
|  |                         audioContent.member.auth.gender.eq( | ||||||
|  |                             if (contentType == ContentType.MALE) { | ||||||
|  |                                 0 | ||||||
|  |                             } else { | ||||||
|  |                                 1 | ||||||
|  |                             } | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (theme.isNotEmpty()) { | ||||||
|  |             where = where.and(audioContentTheme.theme.`in`(theme)) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         where = if (isFree) { | ||||||
|  |             where.and(audioContent.price.loe(0)) | ||||||
|  |         } else { | ||||||
|  |             where.and(audioContent.price.gt(0)) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return queryFactory | ||||||
|  |             .select( | ||||||
|  |                 QAudioContentMainItem( | ||||||
|  |                     audioContent.id, | ||||||
|  |                     member.id, | ||||||
|  |                     audioContent.title, | ||||||
|  |                     audioContent.coverImage.prepend("/").prepend(imageHost), | ||||||
|  |                     member.nickname, | ||||||
|  |                     audioContent.isPointAvailable | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             .from(audioContent) | ||||||
|  |             .innerJoin(audioContent.member, member) | ||||||
|  |             .innerJoin(audioContent.theme, audioContentTheme) | ||||||
|  |             .where(where) | ||||||
|  |             .offset(offset) | ||||||
|  |             .limit(limit) | ||||||
|  |             .fetch() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun findContentByCurationId( | ||||||
|  |         curationId: Long, | ||||||
|  |         isAdult: Boolean, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         offset: Long, | ||||||
|  |         limit: Long | ||||||
|  |     ): List<GetAudioContentMainItem> { | ||||||
|  |         var where = audioContentCuration.isActive.isTrue | ||||||
|  |             .and(audioContentCurationItem.isActive.isTrue) | ||||||
|  |             .and(audioContent.isActive.isTrue) | ||||||
|  |             .and(audioContent.member.isNotNull) | ||||||
|  |             .and(audioContent.duration.isNotNull) | ||||||
|  |             .and(audioContent.member.isActive.isTrue) | ||||||
|  |             .and(audioContentCuration.id.eq(curationId)) | ||||||
|  |  | ||||||
|  |         if (!isAdult) { | ||||||
|  |             where = where.and(audioContent.isAdult.isFalse) | ||||||
|  |         } else { | ||||||
|  |             if (contentType != ContentType.ALL) { | ||||||
|  |                 where = where.and( | ||||||
|  |                     audioContent.member.isNull.or( | ||||||
|  |                         audioContent.member.auth.gender.eq( | ||||||
|  |                             if (contentType == ContentType.MALE) { | ||||||
|  |                                 0 | ||||||
|  |                             } else { | ||||||
|  |                                 1 | ||||||
|  |                             } | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return queryFactory | ||||||
|  |             .select( | ||||||
|  |                 QGetAudioContentMainItem( | ||||||
|  |                     audioContent.id, | ||||||
|  |                     audioContent.coverImage.prepend("/").prepend(imageHost), | ||||||
|  |                     audioContent.title, | ||||||
|  |                     member.id, | ||||||
|  |                     member.profileImage.prepend("/").prepend(imageHost), | ||||||
|  |                     member.nickname, | ||||||
|  |                     audioContent.price, | ||||||
|  |                     audioContent.duration, | ||||||
|  |                     audioContent.isPointAvailable | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             .from(audioContentCurationItem) | ||||||
|  |             .innerJoin(audioContentCurationItem.content, audioContent) | ||||||
|  |             .innerJoin(audioContentCurationItem.curation, audioContentCuration) | ||||||
|  |             .innerJoin(audioContent.member, member) | ||||||
|  |             .where(where) | ||||||
|  |             .orderBy(audioContentCurationItem.orders.asc()) | ||||||
|  |             .offset(offset) | ||||||
|  |             .limit(limit) | ||||||
|  |             .fetch() | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -938,4 +938,22 @@ class AudioContentService( | |||||||
|  |  | ||||||
|         return GenerateUrlResponse(contentUrl) |         return GenerateUrlResponse(contentUrl) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fun getLatestContentByTheme( | ||||||
|  |         theme: List<String>, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         offset: Long = 0, | ||||||
|  |         limit: Long = 20, | ||||||
|  |         isFree: Boolean = false, | ||||||
|  |         isAdult: Boolean = false | ||||||
|  |     ): List<AudioContentMainItem> { | ||||||
|  |         return repository.getLatestContentByTheme( | ||||||
|  |             theme = theme, | ||||||
|  |             contentType = contentType, | ||||||
|  |             offset = offset, | ||||||
|  |             limit = limit, | ||||||
|  |             isFree = isFree, | ||||||
|  |             isAdult = isAdult | ||||||
|  |         ) | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,16 +1,21 @@ | |||||||
| package kr.co.vividnext.sodalive.content.main.curation | package kr.co.vividnext.sodalive.content.main.curation | ||||||
|  |  | ||||||
|  | import kr.co.vividnext.sodalive.content.AudioContentRepository | ||||||
| import kr.co.vividnext.sodalive.content.ContentType | import kr.co.vividnext.sodalive.content.ContentType | ||||||
| import kr.co.vividnext.sodalive.content.SortType | import kr.co.vividnext.sodalive.content.SortType | ||||||
|  | import kr.co.vividnext.sodalive.content.main.tab.GetContentCurationResponse | ||||||
| import kr.co.vividnext.sodalive.member.Member | import kr.co.vividnext.sodalive.member.Member | ||||||
| import kr.co.vividnext.sodalive.member.block.BlockMemberRepository | import kr.co.vividnext.sodalive.member.block.BlockMemberRepository | ||||||
| import org.springframework.beans.factory.annotation.Value | import org.springframework.beans.factory.annotation.Value | ||||||
|  | import org.springframework.cache.annotation.Cacheable | ||||||
| import org.springframework.data.domain.Pageable | import org.springframework.data.domain.Pageable | ||||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||||
|  | import org.springframework.transaction.annotation.Transactional | ||||||
|  |  | ||||||
| @Service | @Service | ||||||
| class AudioContentCurationService( | class AudioContentCurationService( | ||||||
|     private val repository: AudioContentCurationQueryRepository, |     private val repository: AudioContentCurationQueryRepository, | ||||||
|  |     private val contentRepository: AudioContentRepository, | ||||||
|     private val blockMemberRepository: BlockMemberRepository, |     private val blockMemberRepository: BlockMemberRepository, | ||||||
|  |  | ||||||
|     @Value("\${cloud.aws.cloud-front.host}") |     @Value("\${cloud.aws.cloud-front.host}") | ||||||
| @@ -46,4 +51,36 @@ class AudioContentCurationService( | |||||||
|             items = audioContentList |             items = audioContentList | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Transactional(readOnly = true) | ||||||
|  |     @Cacheable( | ||||||
|  |         cacheNames = ["cache_ttl_3_days"], | ||||||
|  |         key = "'getContentCurationList:' + ':' +" + | ||||||
|  |             "#isAdult + ':' + #tabId + ':' + #contentType + ':' + (#memberId ?: 'guest')" | ||||||
|  |     ) | ||||||
|  |     fun getContentCurationList( | ||||||
|  |         tabId: Long, | ||||||
|  |         isAdult: Boolean, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         memberId: Long? | ||||||
|  |     ): List<GetContentCurationResponse> { | ||||||
|  |         return repository.findByContentMainTabId(tabId = tabId, isAdult = isAdult) | ||||||
|  |             .map { | ||||||
|  |                 GetContentCurationResponse( | ||||||
|  |                     title = it.title, | ||||||
|  |                     items = contentRepository.findContentByCurationId( | ||||||
|  |                         curationId = it.id!!, | ||||||
|  |                         isAdult = isAdult, | ||||||
|  |                         contentType = contentType | ||||||
|  |                     ).filter { item -> | ||||||
|  |                         if (memberId != null) { | ||||||
|  |                             !blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = item.creatorId) | ||||||
|  |                         } else { | ||||||
|  |                             true | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |             .filter { it.items.isNotEmpty() } | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -42,7 +42,6 @@ class AudioContentMainTabSeriesController(private val service: AudioContentMainT | |||||||
|  |  | ||||||
|         ApiResponse.ok( |         ApiResponse.ok( | ||||||
|             service.getOriginalAudioDramaList( |             service.getOriginalAudioDramaList( | ||||||
|                 memberId = member.id!!, |  | ||||||
|                 isAdult = member.auth != null && (isAdultContentVisible ?: true), |                 isAdult = member.auth != null && (isAdultContentVisible ?: true), | ||||||
|                 contentType = contentType ?: ContentType.ALL, |                 contentType = contentType ?: ContentType.ALL, | ||||||
|                 offset = pageable.offset, |                 offset = pageable.offset, | ||||||
|   | |||||||
| @@ -41,7 +41,6 @@ class AudioContentMainTabSeriesService( | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         val originalAudioDrama = seriesService.getOriginalAudioDramaList( |         val originalAudioDrama = seriesService.getOriginalAudioDramaList( | ||||||
|             memberId = memberId, |  | ||||||
|             isAdult = isAdult, |             isAdult = isAdult, | ||||||
|             contentType = contentType, |             contentType = contentType, | ||||||
|             offset = 0, |             offset = 0, | ||||||
| @@ -158,15 +157,13 @@ class AudioContentMainTabSeriesService( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun getOriginalAudioDramaList( |     fun getOriginalAudioDramaList( | ||||||
|         memberId: Long, |  | ||||||
|         isAdult: Boolean, |         isAdult: Boolean, | ||||||
|         contentType: ContentType, |         contentType: ContentType, | ||||||
|         offset: Long, |         offset: Long, | ||||||
|         limit: Long |         limit: Long | ||||||
|     ): GetSeriesListResponse { |     ): GetSeriesListResponse { | ||||||
|         val totalCount = seriesService.getOriginalAudioDramaTotalCount(memberId, isAdult, contentType) |         val totalCount = seriesService.getOriginalAudioDramaTotalCount(isAdult, contentType) | ||||||
|         val items = seriesService.getOriginalAudioDramaList( |         val items = seriesService.getOriginalAudioDramaList( | ||||||
|             memberId = memberId, |  | ||||||
|             isAdult = isAdult, |             isAdult = isAdult, | ||||||
|             contentType = contentType, |             contentType = contentType, | ||||||
|             offset = offset, |             offset = offset, | ||||||
|   | |||||||
| @@ -19,7 +19,6 @@ class ContentSeriesController(private val service: ContentSeriesService) { | |||||||
|     @GetMapping |     @GetMapping | ||||||
|     fun getSeriesList( |     fun getSeriesList( | ||||||
|         @RequestParam creatorId: Long, |         @RequestParam creatorId: Long, | ||||||
|         @RequestParam("sortType", required = false) sortType: SeriesSortType? = SeriesSortType.NEWEST, |  | ||||||
|         @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, |         @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, | ||||||
|         @RequestParam("contentType", required = false) contentType: ContentType? = null, |         @RequestParam("contentType", required = false) contentType: ContentType? = null, | ||||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, |         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, | ||||||
| @@ -30,7 +29,6 @@ class ContentSeriesController(private val service: ContentSeriesService) { | |||||||
|         ApiResponse.ok( |         ApiResponse.ok( | ||||||
|             service.getSeriesList( |             service.getSeriesList( | ||||||
|                 creatorId = creatorId, |                 creatorId = creatorId, | ||||||
|                 sortType = sortType ?: SeriesSortType.NEWEST, |  | ||||||
|                 isAdultContentVisible = isAdultContentVisible ?: true, |                 isAdultContentVisible = isAdultContentVisible ?: true, | ||||||
|                 contentType = contentType ?: ContentType.ALL, |                 contentType = contentType ?: ContentType.ALL, | ||||||
|                 member = member, |                 member = member, | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.content.series.content.QGetSeriesContentMinMaxPr | |||||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series | 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.QSeriesContent.seriesContent | ||||||
| import kr.co.vividnext.sodalive.creator.admin.content.series.Series | 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.keyword.QSeriesKeyword.seriesKeyword | 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.MemberRole | ||||||
| import kr.co.vividnext.sodalive.member.QMember.member | import kr.co.vividnext.sodalive.member.QMember.member | ||||||
| @@ -37,16 +38,22 @@ interface ContentSeriesQueryRepository { | |||||||
|     fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse |     fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse | ||||||
|     fun getRecommendSeriesList(isAuth: Boolean, contentType: ContentType, limit: Long): List<Series> |     fun getRecommendSeriesList(isAuth: Boolean, contentType: ContentType, limit: Long): List<Series> | ||||||
|     fun getOriginalAudioDramaList( |     fun getOriginalAudioDramaList( | ||||||
|         memberId: Long, |  | ||||||
|         isAdult: Boolean, |         isAdult: Boolean, | ||||||
|         contentType: ContentType, |         contentType: ContentType, | ||||||
|         offset: Long = 0, |         offset: Long = 0, | ||||||
|         limit: Long = 20 |         limit: Long = 20 | ||||||
|     ): List<Series> |     ): List<Series> | ||||||
|  |  | ||||||
|     fun getOriginalAudioDramaTotalCount(memberId: Long, isAdult: Boolean, contentType: ContentType): Int |     fun getOriginalAudioDramaTotalCount(isAdult: Boolean, contentType: ContentType): Int | ||||||
|     fun getGenreList(isAdult: Boolean, memberId: Long, contentType: ContentType): List<GetSeriesGenreListResponse> |     fun getGenreList(isAdult: Boolean, memberId: Long, contentType: ContentType): List<GetSeriesGenreListResponse> | ||||||
|     fun findByCurationId(curationId: Long, memberId: Long, isAdult: Boolean, contentType: ContentType): List<Series> |     fun findByCurationId(curationId: Long, memberId: Long, isAdult: Boolean, contentType: ContentType): List<Series> | ||||||
|  |     fun getDayOfWeekSeriesList( | ||||||
|  |         dayOfWeek: SeriesPublishedDaysOfWeek, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         isAdult: Boolean, | ||||||
|  |         offset: Long, | ||||||
|  |         limit: Long | ||||||
|  |     ): List<Series> | ||||||
| } | } | ||||||
|  |  | ||||||
| class ContentSeriesQueryRepositoryImpl( | class ContentSeriesQueryRepositoryImpl( | ||||||
| @@ -207,19 +214,13 @@ class ContentSeriesQueryRepositoryImpl( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun getOriginalAudioDramaList( |     override fun getOriginalAudioDramaList( | ||||||
|         memberId: Long, |  | ||||||
|         isAdult: Boolean, |         isAdult: Boolean, | ||||||
|         contentType: ContentType, |         contentType: ContentType, | ||||||
|         offset: Long, |         offset: Long, | ||||||
|         limit: Long |         limit: Long | ||||||
|     ): List<Series> { |     ): List<Series> { | ||||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) |  | ||||||
|             .and(blockMember.isActive.isTrue) |  | ||||||
|             .and(blockMember.blockedMember.id.eq(memberId)) |  | ||||||
|  |  | ||||||
|         var where = series.isOriginal.isTrue |         var where = series.isOriginal.isTrue | ||||||
|             .and(series.isActive.isTrue) |             .and(series.isActive.isTrue) | ||||||
|             .and(blockMember.id.isNull) |  | ||||||
|  |  | ||||||
|         if (!isAdult) { |         if (!isAdult) { | ||||||
|             where = where.and(series.isAdult.isFalse) |             where = where.and(series.isAdult.isFalse) | ||||||
| @@ -242,7 +243,6 @@ class ContentSeriesQueryRepositoryImpl( | |||||||
|         return queryFactory |         return queryFactory | ||||||
|             .selectFrom(series) |             .selectFrom(series) | ||||||
|             .innerJoin(series.member, member) |             .innerJoin(series.member, member) | ||||||
|             .leftJoin(blockMember).on(blockMemberCondition) |  | ||||||
|             .where(where) |             .where(where) | ||||||
|             .orderBy(series.id.desc()) |             .orderBy(series.id.desc()) | ||||||
|             .offset(offset) |             .offset(offset) | ||||||
| @@ -250,14 +250,9 @@ class ContentSeriesQueryRepositoryImpl( | |||||||
|             .fetch() |             .fetch() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun getOriginalAudioDramaTotalCount(memberId: Long, isAdult: Boolean, contentType: ContentType): Int { |     override fun getOriginalAudioDramaTotalCount(isAdult: Boolean, contentType: ContentType): Int { | ||||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) |  | ||||||
|             .and(blockMember.isActive.isTrue) |  | ||||||
|             .and(blockMember.blockedMember.id.eq(memberId)) |  | ||||||
|  |  | ||||||
|         var where = series.isOriginal.isTrue |         var where = series.isOriginal.isTrue | ||||||
|             .and(series.isActive.isTrue) |             .and(series.isActive.isTrue) | ||||||
|             .and(blockMember.id.isNull) |  | ||||||
|  |  | ||||||
|         if (!isAdult) { |         if (!isAdult) { | ||||||
|             where = where.and(series.isAdult.isFalse) |             where = where.and(series.isAdult.isFalse) | ||||||
| @@ -281,7 +276,6 @@ class ContentSeriesQueryRepositoryImpl( | |||||||
|             .select(series.id) |             .select(series.id) | ||||||
|             .from(series) |             .from(series) | ||||||
|             .innerJoin(series.member, member) |             .innerJoin(series.member, member) | ||||||
|             .leftJoin(blockMember).on(blockMemberCondition) |  | ||||||
|             .where(where) |             .where(where) | ||||||
|             .fetch() |             .fetch() | ||||||
|             .size |             .size | ||||||
| @@ -385,4 +379,44 @@ class ContentSeriesQueryRepositoryImpl( | |||||||
|             .orderBy(audioContentCurationItem.orders.asc()) |             .orderBy(audioContentCurationItem.orders.asc()) | ||||||
|             .fetch() |             .fetch() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override fun getDayOfWeekSeriesList( | ||||||
|  |         dayOfWeek: SeriesPublishedDaysOfWeek, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         isAdult: Boolean, | ||||||
|  |         offset: Long, | ||||||
|  |         limit: Long | ||||||
|  |     ): List<Series> { | ||||||
|  |         var where = series.isActive.isTrue | ||||||
|  |             .and(series.publishedDaysOfWeek.contains(dayOfWeek)) | ||||||
|  |  | ||||||
|  |         if (!isAdult) { | ||||||
|  |             where = where.and(series.isAdult.isFalse) | ||||||
|  |         } else { | ||||||
|  |             if (contentType != ContentType.ALL) { | ||||||
|  |                 where = where.and( | ||||||
|  |                     series.member.isNull.or( | ||||||
|  |                         series.member.auth.gender.eq( | ||||||
|  |                             if (contentType == ContentType.MALE) { | ||||||
|  |                                 0 | ||||||
|  |                             } else { | ||||||
|  |                                 1 | ||||||
|  |                             } | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return queryFactory | ||||||
|  |             .selectFrom(series) | ||||||
|  |             .innerJoin(series.member, member) | ||||||
|  |             .innerJoin(series.contentList, seriesContent) | ||||||
|  |             .innerJoin(seriesContent.content, audioContent) | ||||||
|  |             .where(where) | ||||||
|  |             .orderBy(seriesContent.content.createdAt.desc()) | ||||||
|  |             .offset(offset) | ||||||
|  |             .limit(limit) | ||||||
|  |             .fetch() | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -30,18 +30,17 @@ class ContentSeriesService( | |||||||
|     @Value("\${cloud.aws.cloud-front.host}") |     @Value("\${cloud.aws.cloud-front.host}") | ||||||
|     private val coverImageHost: String |     private val coverImageHost: String | ||||||
| ) { | ) { | ||||||
|     fun getOriginalAudioDramaTotalCount(memberId: Long, isAdult: Boolean, contentType: ContentType): Int { |     fun getOriginalAudioDramaTotalCount(isAdult: Boolean, contentType: ContentType): Int { | ||||||
|         return repository.getOriginalAudioDramaTotalCount(memberId, isAdult, contentType) |         return repository.getOriginalAudioDramaTotalCount(isAdult, contentType) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun getOriginalAudioDramaList( |     fun getOriginalAudioDramaList( | ||||||
|         memberId: Long, |  | ||||||
|         isAdult: Boolean, |         isAdult: Boolean, | ||||||
|         contentType: ContentType, |         contentType: ContentType, | ||||||
|         offset: Long = 0, |         offset: Long = 0, | ||||||
|         limit: Long = 20 |         limit: Long = 20 | ||||||
|     ): List<GetSeriesListResponse.SeriesListItem> { |     ): List<GetSeriesListResponse.SeriesListItem> { | ||||||
|         val originalAudioDramaList = repository.getOriginalAudioDramaList(memberId, isAdult, contentType, offset, limit) |         val originalAudioDramaList = repository.getOriginalAudioDramaList(isAdult, contentType, offset, limit) | ||||||
|         return seriesToSeriesListItem(originalAudioDramaList, isAdult, contentType) |         return seriesToSeriesListItem(originalAudioDramaList, isAdult, contentType) | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -54,7 +53,6 @@ class ContentSeriesService( | |||||||
|         isAdultContentVisible: Boolean, |         isAdultContentVisible: Boolean, | ||||||
|         contentType: ContentType, |         contentType: ContentType, | ||||||
|         member: Member, |         member: Member, | ||||||
|         sortType: SeriesSortType = SeriesSortType.NEWEST, |  | ||||||
|         offset: Long = 0, |         offset: Long = 0, | ||||||
|         limit: Long = 10 |         limit: Long = 10 | ||||||
|     ): GetSeriesListResponse { |     ): GetSeriesListResponse { | ||||||
| @@ -224,6 +222,36 @@ class ContentSeriesService( | |||||||
|         return seriesToSeriesListItem(seriesList, isAdult, contentType) |         return seriesToSeriesListItem(seriesList, isAdult, contentType) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     fun getDayOfWeekSeriesList( | ||||||
|  |         memberId: Long?, | ||||||
|  |         isAdult: Boolean, | ||||||
|  |         contentType: ContentType, | ||||||
|  |         dayOfWeek: SeriesPublishedDaysOfWeek, | ||||||
|  |         offset: Long = 0, | ||||||
|  |         limit: Long = 10 | ||||||
|  |     ): List<GetSeriesListResponse.SeriesListItem> { | ||||||
|  |         var seriesList = repository.getDayOfWeekSeriesList( | ||||||
|  |             dayOfWeek = dayOfWeek, | ||||||
|  |             contentType = contentType, | ||||||
|  |             isAdult = isAdult, | ||||||
|  |             offset = offset, | ||||||
|  |             limit = limit | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         seriesList = if (memberId != null) { | ||||||
|  |             seriesList.filter { | ||||||
|  |                 !blockMemberRepository.isBlocked( | ||||||
|  |                     blockedMemberId = memberId, | ||||||
|  |                     memberId = it.member!!.id!! | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             seriesList | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return seriesToSeriesListItem(seriesList, isAdult, contentType) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private fun seriesToSeriesListItem( |     private fun seriesToSeriesListItem( | ||||||
|         seriesList: List<Series>, |         seriesList: List<Series>, | ||||||
|         isAdult: Boolean, |         isAdult: Boolean, | ||||||
|   | |||||||
| @@ -19,6 +19,19 @@ class AudioContentThemeService( | |||||||
|         return queryRepository.getActiveThemes() |         return queryRepository.getActiveThemes() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Transactional(readOnly = true) | ||||||
|  |     fun getActiveThemeOfContent( | ||||||
|  |         isAdult: Boolean = false, | ||||||
|  |         isFree: Boolean = false, | ||||||
|  |         contentType: ContentType | ||||||
|  |     ): List<String> { | ||||||
|  |         return queryRepository.getActiveThemeOfContent( | ||||||
|  |             isAdult = isAdult, | ||||||
|  |             isFree = isFree, | ||||||
|  |             contentType = contentType | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Transactional(readOnly = true) |     @Transactional(readOnly = true) | ||||||
|     fun getContentByTheme( |     fun getContentByTheme( | ||||||
|         themeId: Long, |         themeId: Long, | ||||||
|   | |||||||
| @@ -26,7 +26,6 @@ class EventService( | |||||||
|     @Transactional(readOnly = true) |     @Transactional(readOnly = true) | ||||||
|     fun getEventList(isAdult: Boolean? = null): GetEventResponse { |     fun getEventList(isAdult: Boolean? = null): GetEventResponse { | ||||||
|         val eventList = repository.getEventList(isAdult) |         val eventList = repository.getEventList(isAdult) | ||||||
|             .asSequence() |  | ||||||
|             .map { |             .map { | ||||||
|                 if (!it.thumbnailImageUrl.startsWith("https://")) { |                 if (!it.thumbnailImageUrl.startsWith("https://")) { | ||||||
|                     it.thumbnailImageUrl = "$cloudFrontHost/${it.thumbnailImageUrl}" |                     it.thumbnailImageUrl = "$cloudFrontHost/${it.thumbnailImageUrl}" | ||||||
| @@ -42,7 +41,6 @@ class EventService( | |||||||
|  |  | ||||||
|                 it |                 it | ||||||
|             } |             } | ||||||
|             .toList() |  | ||||||
|  |  | ||||||
|         return GetEventResponse(0, eventList) |         return GetEventResponse(0, eventList) | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -0,0 +1,82 @@ | |||||||
|  | package kr.co.vividnext.sodalive.query.recommend | ||||||
|  |  | ||||||
|  | import com.querydsl.core.types.dsl.Expressions | ||||||
|  | import com.querydsl.jpa.impl.JPAQueryFactory | ||||||
|  | import kr.co.vividnext.sodalive.content.ContentType | ||||||
|  | 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.member.MemberRole | ||||||
|  | import kr.co.vividnext.sodalive.member.QMember.member | ||||||
|  | import kr.co.vividnext.sodalive.member.auth.QAuth.auth | ||||||
|  | import org.springframework.beans.factory.annotation.Value | ||||||
|  | import org.springframework.stereotype.Repository | ||||||
|  |  | ||||||
|  | @Repository | ||||||
|  | class RecommendChannelQueryRepository( | ||||||
|  |     private val queryFactory: JPAQueryFactory, | ||||||
|  |  | ||||||
|  |     @Value("\${cloud.aws.cloud-front.host}") | ||||||
|  |     private val imageHost: String | ||||||
|  | ) { | ||||||
|  |     fun getRecommendChannelList(isAdult: Boolean, contentType: ContentType): List<RecommendChannelResponse> { | ||||||
|  |         var where = member.role.eq(MemberRole.CREATOR) | ||||||
|  |             .and(audioContent.isActive.isTrue) | ||||||
|  |  | ||||||
|  |         if (!isAdult) { | ||||||
|  |             where = where.and(audioContent.isAdult.isFalse) | ||||||
|  |         } else { | ||||||
|  |             if (contentType != ContentType.ALL) { | ||||||
|  |                 where = where.and( | ||||||
|  |                     member.auth.gender.eq( | ||||||
|  |                         if (contentType == ContentType.MALE) { | ||||||
|  |                             0 | ||||||
|  |                         } else { | ||||||
|  |                             1 | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return queryFactory | ||||||
|  |             .select( | ||||||
|  |                 QRecommendChannelResponse( | ||||||
|  |                     member.id, | ||||||
|  |                     member.nickname, | ||||||
|  |                     member.profileImage.prepend("/").prepend(imageHost), | ||||||
|  |                     audioContent.id.count(), | ||||||
|  |                     Expressions.constant(emptyList<RecommendChannelContentItem>()) | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             .from(member) | ||||||
|  |             .innerJoin(auth).on(auth.member.id.eq(member.id)) | ||||||
|  |             .innerJoin(audioContent).on(audioContent.member.id.eq(member.id)) | ||||||
|  |             .where(where) | ||||||
|  |             .groupBy(member.id) | ||||||
|  |             .having(audioContent.id.count().goe(3)) | ||||||
|  |             .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) | ||||||
|  |             .limit(6) | ||||||
|  |             .fetch() | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun getContentsByCreatorIdLikeDesc(creatorId: Long): List<RecommendChannelContentItem> { | ||||||
|  |         queryFactory | ||||||
|  |             .select( | ||||||
|  |                 QRecommendChannelContentItem( | ||||||
|  |                     audioContent.id, | ||||||
|  |                     audioContent.title, | ||||||
|  |                     audioContent.coverImage.prepend("/").prepend(imageHost), | ||||||
|  |                     audioContentLike.id.countDistinct(), | ||||||
|  |                     audioContentComment.id.countDistinct() | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             .from(audioContent) | ||||||
|  |             .leftJoin(audioContentLike).on(audioContentLike.audioContent.id.eq(audioContent.id)) | ||||||
|  |             .leftJoin(audioContentComment).on(audioContentComment.audioContent.id.eq(audioContent.id)) | ||||||
|  |             .where(audioContent.member.id.eq(creatorId)) | ||||||
|  |             .groupBy(audioContent.id) | ||||||
|  |             .fetch() | ||||||
|  |         return listOf() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | package kr.co.vividnext.sodalive.query.recommend | ||||||
|  |  | ||||||
|  | import kr.co.vividnext.sodalive.content.ContentType | ||||||
|  | import org.springframework.cache.annotation.Cacheable | ||||||
|  | import org.springframework.stereotype.Service | ||||||
|  | import org.springframework.transaction.annotation.Transactional | ||||||
|  |  | ||||||
|  | @Service | ||||||
|  | @Transactional(readOnly = true) | ||||||
|  | class RecommendChannelQueryService(private val repository: RecommendChannelQueryRepository) { | ||||||
|  |     @Cacheable( | ||||||
|  |         cacheNames = ["default"], | ||||||
|  |         key = "'recommendChannel:' + (#memberId ?: 'guest') + ':' + #isAdult + ':' + #contentType" | ||||||
|  |     ) | ||||||
|  |     fun getRecommendChannel( | ||||||
|  |         memberId: Long?, | ||||||
|  |         isAdult: Boolean, | ||||||
|  |         contentType: ContentType | ||||||
|  |     ): List<RecommendChannelResponse> { | ||||||
|  |         val recommendChannelList = repository.getRecommendChannelList( | ||||||
|  |             isAdult = isAdult, | ||||||
|  |             contentType = contentType | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         return recommendChannelList.map { | ||||||
|  |             it.contentList = repository.getContentsByCreatorIdLikeDesc(it.channelId) | ||||||
|  |             it | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | package kr.co.vividnext.sodalive.query.recommend | ||||||
|  |  | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty | ||||||
|  | import com.querydsl.core.annotations.QueryProjection | ||||||
|  |  | ||||||
|  | data class RecommendChannelResponse @QueryProjection constructor( | ||||||
|  |     @JsonProperty("channelId") val channelId: Long, | ||||||
|  |     @JsonProperty("creatorNickname") val creatorNickname: String, | ||||||
|  |     @JsonProperty("creatorProfileImageUrl") val creatorProfileImageUrl: String, | ||||||
|  |     @JsonProperty("contentCount") val contentCount: Long, | ||||||
|  |     @JsonProperty("contentList") var contentList: List<RecommendChannelContentItem> | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | data class RecommendChannelContentItem @QueryProjection constructor( | ||||||
|  |     @JsonProperty("contentId") val contentId: Long, | ||||||
|  |     @JsonProperty("title") val title: String, | ||||||
|  |     @JsonProperty("thumbnailImageUrl") val thumbnailImageUrl: String, | ||||||
|  |     @JsonProperty("likeCount") val likeCount: Long, | ||||||
|  |     @JsonProperty("commentCount") val commentCount: Long | ||||||
|  | ) | ||||||
		Reference in New Issue
	
	Block a user