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 getAuditionList(offset: Long, limit: Long, isAdult: Boolean): List<GetAuditionListItem> | ||||
|     fun getAuditionDetail(auditionId: Long): GetAuditionDetailRawData | ||||
|     fun getInProgressAuditionList(isAdult: Boolean): List<GetAuditionListItem> | ||||
| } | ||||
|  | ||||
| class AuditionQueryRepositoryImpl( | ||||
| @@ -94,4 +95,27 @@ class AuditionQueryRepositoryImpl( | ||||
|             .where(audition.id.eq(auditionId)) | ||||
|             .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 | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     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 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 | ||||
| @@ -1281,4 +1298,126 @@ class AudioContentQueryRepositoryImpl( | ||||
|             .orderBy(audioContentHashTag.id.asc()) | ||||
|             .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) | ||||
|     } | ||||
|  | ||||
|     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 | ||||
|  | ||||
| import kr.co.vividnext.sodalive.content.AudioContentRepository | ||||
| import kr.co.vividnext.sodalive.content.ContentType | ||||
| 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.block.BlockMemberRepository | ||||
| import org.springframework.beans.factory.annotation.Value | ||||
| import org.springframework.cache.annotation.Cacheable | ||||
| import org.springframework.data.domain.Pageable | ||||
| import org.springframework.stereotype.Service | ||||
| import org.springframework.transaction.annotation.Transactional | ||||
|  | ||||
| @Service | ||||
| class AudioContentCurationService( | ||||
|     private val repository: AudioContentCurationQueryRepository, | ||||
|     private val contentRepository: AudioContentRepository, | ||||
|     private val blockMemberRepository: BlockMemberRepository, | ||||
|  | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
| @@ -46,4 +51,36 @@ class AudioContentCurationService( | ||||
|             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( | ||||
|             service.getOriginalAudioDramaList( | ||||
|                 memberId = member.id!!, | ||||
|                 isAdult = member.auth != null && (isAdultContentVisible ?: true), | ||||
|                 contentType = contentType ?: ContentType.ALL, | ||||
|                 offset = pageable.offset, | ||||
|   | ||||
| @@ -41,7 +41,6 @@ class AudioContentMainTabSeriesService( | ||||
|         ) | ||||
|  | ||||
|         val originalAudioDrama = seriesService.getOriginalAudioDramaList( | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult, | ||||
|             contentType = contentType, | ||||
|             offset = 0, | ||||
| @@ -158,15 +157,13 @@ class AudioContentMainTabSeriesService( | ||||
|     } | ||||
|  | ||||
|     fun getOriginalAudioDramaList( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         contentType: ContentType, | ||||
|         offset: Long, | ||||
|         limit: Long | ||||
|     ): GetSeriesListResponse { | ||||
|         val totalCount = seriesService.getOriginalAudioDramaTotalCount(memberId, isAdult, contentType) | ||||
|         val totalCount = seriesService.getOriginalAudioDramaTotalCount(isAdult, contentType) | ||||
|         val items = seriesService.getOriginalAudioDramaList( | ||||
|             memberId = memberId, | ||||
|             isAdult = isAdult, | ||||
|             contentType = contentType, | ||||
|             offset = offset, | ||||
|   | ||||
| @@ -19,7 +19,6 @@ class ContentSeriesController(private val service: ContentSeriesService) { | ||||
|     @GetMapping | ||||
|     fun getSeriesList( | ||||
|         @RequestParam creatorId: Long, | ||||
|         @RequestParam("sortType", required = false) sortType: SeriesSortType? = SeriesSortType.NEWEST, | ||||
|         @RequestParam("isAdultContentVisible", required = false) isAdultContentVisible: Boolean? = null, | ||||
|         @RequestParam("contentType", required = false) contentType: ContentType? = null, | ||||
|         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, | ||||
| @@ -30,7 +29,6 @@ class ContentSeriesController(private val service: ContentSeriesService) { | ||||
|         ApiResponse.ok( | ||||
|             service.getSeriesList( | ||||
|                 creatorId = creatorId, | ||||
|                 sortType = sortType ?: SeriesSortType.NEWEST, | ||||
|                 isAdultContentVisible = isAdultContentVisible ?: true, | ||||
|                 contentType = contentType ?: ContentType.ALL, | ||||
|                 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.QSeriesContent.seriesContent | ||||
| 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.member.MemberRole | ||||
| import kr.co.vividnext.sodalive.member.QMember.member | ||||
| @@ -37,16 +38,22 @@ interface ContentSeriesQueryRepository { | ||||
|     fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse | ||||
|     fun getRecommendSeriesList(isAuth: Boolean, contentType: ContentType, limit: Long): List<Series> | ||||
|     fun getOriginalAudioDramaList( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         contentType: ContentType, | ||||
|         offset: Long = 0, | ||||
|         limit: Long = 20 | ||||
|     ): 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 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( | ||||
| @@ -207,19 +214,13 @@ class ContentSeriesQueryRepositoryImpl( | ||||
|     } | ||||
|  | ||||
|     override fun getOriginalAudioDramaList( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         contentType: ContentType, | ||||
|         offset: Long, | ||||
|         limit: Long | ||||
|     ): List<Series> { | ||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) | ||||
|             .and(blockMember.isActive.isTrue) | ||||
|             .and(blockMember.blockedMember.id.eq(memberId)) | ||||
|  | ||||
|         var where = series.isOriginal.isTrue | ||||
|             .and(series.isActive.isTrue) | ||||
|             .and(blockMember.id.isNull) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
| @@ -242,7 +243,6 @@ class ContentSeriesQueryRepositoryImpl( | ||||
|         return queryFactory | ||||
|             .selectFrom(series) | ||||
|             .innerJoin(series.member, member) | ||||
|             .leftJoin(blockMember).on(blockMemberCondition) | ||||
|             .where(where) | ||||
|             .orderBy(series.id.desc()) | ||||
|             .offset(offset) | ||||
| @@ -250,14 +250,9 @@ class ContentSeriesQueryRepositoryImpl( | ||||
|             .fetch() | ||||
|     } | ||||
|  | ||||
|     override fun getOriginalAudioDramaTotalCount(memberId: Long, isAdult: Boolean, contentType: ContentType): Int { | ||||
|         val blockMemberCondition = blockMember.member.id.eq(member.id) | ||||
|             .and(blockMember.isActive.isTrue) | ||||
|             .and(blockMember.blockedMember.id.eq(memberId)) | ||||
|  | ||||
|     override fun getOriginalAudioDramaTotalCount(isAdult: Boolean, contentType: ContentType): Int { | ||||
|         var where = series.isOriginal.isTrue | ||||
|             .and(series.isActive.isTrue) | ||||
|             .and(blockMember.id.isNull) | ||||
|  | ||||
|         if (!isAdult) { | ||||
|             where = where.and(series.isAdult.isFalse) | ||||
| @@ -281,7 +276,6 @@ class ContentSeriesQueryRepositoryImpl( | ||||
|             .select(series.id) | ||||
|             .from(series) | ||||
|             .innerJoin(series.member, member) | ||||
|             .leftJoin(blockMember).on(blockMemberCondition) | ||||
|             .where(where) | ||||
|             .fetch() | ||||
|             .size | ||||
| @@ -385,4 +379,44 @@ class ContentSeriesQueryRepositoryImpl( | ||||
|             .orderBy(audioContentCurationItem.orders.asc()) | ||||
|             .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}") | ||||
|     private val coverImageHost: String | ||||
| ) { | ||||
|     fun getOriginalAudioDramaTotalCount(memberId: Long, isAdult: Boolean, contentType: ContentType): Int { | ||||
|         return repository.getOriginalAudioDramaTotalCount(memberId, isAdult, contentType) | ||||
|     fun getOriginalAudioDramaTotalCount(isAdult: Boolean, contentType: ContentType): Int { | ||||
|         return repository.getOriginalAudioDramaTotalCount(isAdult, contentType) | ||||
|     } | ||||
|  | ||||
|     fun getOriginalAudioDramaList( | ||||
|         memberId: Long, | ||||
|         isAdult: Boolean, | ||||
|         contentType: ContentType, | ||||
|         offset: Long = 0, | ||||
|         limit: Long = 20 | ||||
|     ): 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) | ||||
|     } | ||||
|  | ||||
| @@ -54,7 +53,6 @@ class ContentSeriesService( | ||||
|         isAdultContentVisible: Boolean, | ||||
|         contentType: ContentType, | ||||
|         member: Member, | ||||
|         sortType: SeriesSortType = SeriesSortType.NEWEST, | ||||
|         offset: Long = 0, | ||||
|         limit: Long = 10 | ||||
|     ): GetSeriesListResponse { | ||||
| @@ -224,6 +222,36 @@ class ContentSeriesService( | ||||
|         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( | ||||
|         seriesList: List<Series>, | ||||
|         isAdult: Boolean, | ||||
|   | ||||
| @@ -19,6 +19,19 @@ class AudioContentThemeService( | ||||
|         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) | ||||
|     fun getContentByTheme( | ||||
|         themeId: Long, | ||||
|   | ||||
| @@ -26,7 +26,6 @@ class EventService( | ||||
|     @Transactional(readOnly = true) | ||||
|     fun getEventList(isAdult: Boolean? = null): GetEventResponse { | ||||
|         val eventList = repository.getEventList(isAdult) | ||||
|             .asSequence() | ||||
|             .map { | ||||
|                 if (!it.thumbnailImageUrl.startsWith("https://")) { | ||||
|                     it.thumbnailImageUrl = "$cloudFrontHost/${it.thumbnailImageUrl}" | ||||
| @@ -42,7 +41,6 @@ class EventService( | ||||
|  | ||||
|                 it | ||||
|             } | ||||
|             .toList() | ||||
|  | ||||
|         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