test #388

Merged
klaus merged 15 commits from test into main 2026-02-13 09:14:20 +00:00
3 changed files with 634 additions and 288 deletions
Showing only changes of commit 01a1a05d77 - Show all commits

View File

@@ -12,7 +12,6 @@ import kr.co.vividnext.sodalive.content.main.GetAudioContentRankingItem
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerService
import kr.co.vividnext.sodalive.content.series.ContentSeriesService import kr.co.vividnext.sodalive.content.series.ContentSeriesService
import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse import kr.co.vividnext.sodalive.content.series.GetSeriesListResponse
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository
import kr.co.vividnext.sodalive.content.theme.AudioContentThemeService import kr.co.vividnext.sodalive.content.theme.AudioContentThemeService
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.event.GetEventResponse import kr.co.vividnext.sodalive.event.GetEventResponse
@@ -50,7 +49,6 @@ class HomeService(
private val explorerQueryRepository: ExplorerQueryRepository, private val explorerQueryRepository: ExplorerQueryRepository,
private val aiCharacterTranslationRepository: AiCharacterTranslationRepository, private val aiCharacterTranslationRepository: AiCharacterTranslationRepository,
private val seriesTranslationRepository: SeriesTranslationRepository,
private val langContext: LangContext, private val langContext: LangContext,
@@ -139,13 +137,12 @@ class HomeService(
val auditionList = auditionService.getInProgressAuditionList(isAdult = isAdult) val auditionList = auditionService.getInProgressAuditionList(isAdult = isAdult)
// 요일별 시리즈 // 요일별 시리즈
val dayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList( val translatedDayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList(
memberId = memberId, memberId = memberId,
isAdult = isAdult, isAdult = isAdult,
contentType = contentType, contentType = contentType,
dayOfWeek = getDayOfWeekByTimezone(timezone) dayOfWeek = getDayOfWeekByTimezone(timezone)
) )
val translatedDayOfWeekSeriesList = getTranslatedSeriesList(seriesList = dayOfWeekSeriesList)
// 인기 캐릭터 조회 // 인기 캐릭터 조회
val translatedPopularCharacters = getTranslatedAiCharacterList(aiCharacterList = characterService.getPopularCharacters()) val translatedPopularCharacters = getTranslatedAiCharacterList(aiCharacterList = characterService.getPopularCharacters())
@@ -265,14 +262,12 @@ class HomeService(
val memberId = member?.id val memberId = member?.id
val isAdult = member?.auth != null && isAdultContentVisible val isAdult = member?.auth != null && isAdultContentVisible
val dayOfWeekSeriesList = seriesService.getDayOfWeekSeriesList( return seriesService.getDayOfWeekSeriesList(
memberId = memberId, memberId = memberId,
isAdult = isAdult, isAdult = isAdult,
contentType = contentType, contentType = contentType,
dayOfWeek = dayOfWeek dayOfWeek = dayOfWeek
) )
return getTranslatedSeriesList(seriesList = dayOfWeekSeriesList)
} }
fun getContentRankingBySort( fun getContentRankingBySort(
@@ -479,44 +474,6 @@ class HomeService(
return result.take(targetSize).shuffled() return result.take(targetSize).shuffled()
} }
/**
* 시리즈 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다.
*
* 처리 절차:
* - 입력된 시리즈들의 seriesId 집합을 만들고, 요청 언어 코드(langContext.lang.code)로
* seriesTranslationRepository에서 번역 데이터를 한 번에 조회한다.
* - 각 항목에 대해 번역된 제목이 존재하고 비어있지 않으면 title만 번역 값으로 교체한다.
* - 번역이 없거나 공백이면 원본 항목을 그대로 반환한다.
*
* 성능:
* - N건의 항목을 1회의 조회로 해결하기 위해 IN 쿼리를 사용한다.
*
* @param seriesList 번역 대상 SeriesListItem 목록
* @return 제목이 가능한 항목은 번역된 목록(불변 사본), 그 외는 원본 항목 유지
*/
private fun getTranslatedSeriesList(
seriesList: List<GetSeriesListResponse.SeriesListItem>
): List<GetSeriesListResponse.SeriesListItem> {
val seriesIds = seriesList.map { it.seriesId }
return if (seriesIds.isNotEmpty()) {
val translations = seriesTranslationRepository
.findBySeriesIdInAndLocale(seriesIds = seriesIds, locale = langContext.lang.code)
.associateBy { it.seriesId }
seriesList.map { item ->
val translatedTitle = translations[item.seriesId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
}
} else {
seriesList
}
}
/** /**
* AI 캐릭터 리스트의 이름/설명을 현재 언어(locale)에 맞춰 일괄 번역한다. * AI 캐릭터 리스트의 이름/설명을 현재 언어(locale)에 맞춰 일괄 번역한다.
* *

View File

@@ -31,10 +31,11 @@ interface ContentSeriesQueryRepository {
isAuth: Boolean, isAuth: Boolean,
contentType: ContentType, contentType: ContentType,
isOriginal: Boolean, isOriginal: Boolean,
isCompleted: Boolean isCompleted: Boolean,
memberId: Long? = null
): Int ): Int
fun getSeriesList( fun getSeriesListV2(
imageHost: String, imageHost: String,
creatorId: Long?, creatorId: Long?,
isAuth: Boolean, isAuth: Boolean,
@@ -43,28 +44,41 @@ interface ContentSeriesQueryRepository {
isCompleted: Boolean, isCompleted: Boolean,
orderByRandom: Boolean, orderByRandom: Boolean,
offset: Long, offset: Long,
limit: Long limit: Long,
): List<Series> locale: String,
memberId: Long? = null
): List<GetSeriesListResponse.SeriesListItem>
fun getSeriesByGenreTotalCount( fun getSeriesByGenreTotalCount(
genreId: Long, genreId: Long,
isAuth: Boolean, isAuth: Boolean,
contentType: ContentType contentType: ContentType,
memberId: Long? = null
): Int ): Int
fun getSeriesByGenreList( fun getSeriesByGenreListV2(
imageHost: String, imageHost: String,
genreId: Long, genreId: Long,
isAuth: Boolean, isAuth: Boolean,
contentType: ContentType, contentType: ContentType,
offset: Long, offset: Long,
limit: Long limit: Long,
): List<Series> locale: String,
memberId: Long? = null
): List<GetSeriesListResponse.SeriesListItem>
fun getSeriesDetail(seriesId: Long, isAuth: Boolean, contentType: ContentType): Series? fun getSeriesDetail(seriesId: Long, isAuth: Boolean, contentType: ContentType): Series?
fun getKeywordList(seriesId: Long): List<String> fun getKeywordList(seriesId: Long): List<String>
fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse fun getSeriesContentMinMaxPrice(seriesId: Long): GetSeriesContentMinMaxPriceResponse
fun getRecommendSeriesList(isAuth: Boolean, contentType: ContentType, limit: Long): List<Series> fun getRecommendSeriesListV2(
imageHost: String,
isAuth: Boolean,
contentType: ContentType,
limit: Long,
locale: String,
memberId: Long? = null
): List<GetSeriesListResponse.SeriesListItem>
fun getOriginalAudioDramaList( fun getOriginalAudioDramaList(
imageHost: String, imageHost: String,
isAdult: Boolean, isAdult: Boolean,
@@ -76,27 +90,57 @@ interface ContentSeriesQueryRepository {
fun getOriginalAudioDramaTotalCount(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 findByCurationIdV2(
fun getDayOfWeekSeriesList( imageHost: String,
curationId: Long,
memberId: Long,
isAdult: Boolean,
contentType: ContentType,
locale: String
): List<GetSeriesListResponse.SeriesListItem>
fun getDayOfWeekSeriesListV2(
imageHost: String,
dayOfWeek: SeriesPublishedDaysOfWeek, dayOfWeek: SeriesPublishedDaysOfWeek,
contentType: ContentType, contentType: ContentType,
isAdult: Boolean, isAdult: Boolean,
offset: Long, offset: Long,
limit: Long limit: Long,
): List<Series> locale: String,
memberId: Long? = null
): List<GetSeriesListResponse.SeriesListItem>
} }
class ContentSeriesQueryRepositoryImpl( class ContentSeriesQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory private val queryFactory: JPAQueryFactory
) : ContentSeriesQueryRepository { ) : ContentSeriesQueryRepository {
private fun parsePublishedDaysOfWeek(raw: String?): Set<SeriesPublishedDaysOfWeek> {
if (raw.isNullOrBlank()) {
return emptySet()
}
return raw.split(",")
.mapNotNull { value ->
val trimmed = value.trim()
if (trimmed.isEmpty()) {
null
} else {
runCatching { SeriesPublishedDaysOfWeek.valueOf(trimmed) }.getOrNull()
}
}
.toSet()
}
override fun getSeriesTotalCount( override fun getSeriesTotalCount(
creatorId: Long?, creatorId: Long?,
isAuth: Boolean, isAuth: Boolean,
contentType: ContentType, contentType: ContentType,
isOriginal: Boolean, isOriginal: Boolean,
isCompleted: Boolean isCompleted: Boolean,
memberId: Long?
): Int { ): Int {
var where = series.isActive.isTrue var where = series.isActive.isTrue
.and(audioContent.isActive.isTrue)
if (creatorId != null) { if (creatorId != null) {
where = where.and(series.member.id.eq(creatorId)) where = where.and(series.member.id.eq(creatorId))
@@ -111,7 +155,9 @@ class ContentSeriesQueryRepositoryImpl(
} }
if (!isAuth) { if (!isAuth) {
where = where.and(series.isAdult.isFalse) where = where
.and(series.isAdult.isFalse)
.and(audioContent.isAdult.isFalse)
} else { } else {
if (contentType != ContentType.ALL) { if (contentType != ContentType.ALL) {
where = where.and( where = where.and(
@@ -128,16 +174,31 @@ class ContentSeriesQueryRepositoryImpl(
} }
} }
return queryFactory if (memberId != null) {
.select(series.id) val blockedSubquery = queryFactory
.from(series) .select(blockMember.id)
.innerJoin(series.member, member) .from(blockMember)
.where(where) .where(
.fetch() blockMember.member.id.eq(series.member.id),
.size blockMember.blockedMember.id.eq(memberId),
blockMember.isActive.isTrue
)
where = where.and(blockedSubquery.exists().not())
} }
override fun getSeriesList( return (
queryFactory
.select(series.id.countDistinct())
.from(series)
.innerJoin(series.member, member)
.innerJoin(seriesContent).on(series.id.eq(seriesContent.series.id))
.innerJoin(seriesContent.content, audioContent)
.where(where)
.fetchOne() ?: 0L
).toInt()
}
override fun getSeriesListV2(
imageHost: String, imageHost: String,
creatorId: Long?, creatorId: Long?,
isAuth: Boolean, isAuth: Boolean,
@@ -146,9 +207,12 @@ class ContentSeriesQueryRepositoryImpl(
isCompleted: Boolean, isCompleted: Boolean,
orderByRandom: Boolean, orderByRandom: Boolean,
offset: Long, offset: Long,
limit: Long limit: Long,
): List<Series> { locale: String,
memberId: Long?
): List<GetSeriesListResponse.SeriesListItem> {
var where = series.isActive.isTrue var where = series.isActive.isTrue
.and(audioContent.isActive.isTrue)
if (creatorId != null) { if (creatorId != null) {
where = where.and(series.member.id.eq(creatorId)) where = where.and(series.member.id.eq(creatorId))
@@ -162,54 +226,134 @@ class ContentSeriesQueryRepositoryImpl(
} }
if (!isAuth) { if (!isAuth) {
where = where.and(series.isAdult.isFalse) where = where
.and(series.isAdult.isFalse)
.and(audioContent.isAdult.isFalse)
} else { } else {
if (contentType != ContentType.ALL) { if (contentType != ContentType.ALL) {
where = where.and( where = where.and(
series.member.isNull.or( series.member.isNull.or(
series.member.auth.gender.eq( series.member.auth.gender.eq(
if (contentType == ContentType.MALE) { if (contentType == ContentType.MALE) 0 else 1
0
} else {
1
}
) )
) )
) )
} }
} }
// 차단 필터
if (memberId != null) {
val blockedSubquery = queryFactory
.select(blockMember.id)
.from(blockMember)
.where(
blockMember.member.id.eq(series.member.id),
blockMember.blockedMember.id.eq(memberId),
blockMember.isActive.isTrue
)
where = where.and(blockedSubquery.exists().not())
}
val latestReleaseDate = audioContent.releaseDate.max()
val orderBy = if (orderByRandom) { val orderBy = if (orderByRandom) {
listOf(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) listOf(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
} else if (creatorId != null) { } else if (creatorId != null) {
listOf(series.orders.asc(), series.createdAt.asc()) listOf(series.orders.asc(), series.createdAt.asc())
} else { } else {
listOf(audioContent.releaseDate.max().desc(), series.createdAt.asc()) listOf(latestReleaseDate.desc(), series.createdAt.asc())
} }
return queryFactory val now = LocalDateTime.now()
.selectFrom(series) val sevenDaysAgo = now.minusDays(7)
val contentCount = seriesContent.id.countDistinct()
val isNewCase = Expressions.numberTemplate(
Int::class.java,
"case when {0} then 1 else 0 end",
audioContent.releaseDate.between(sevenDaysAgo, now)
)
val isNewFlag = isNewCase.max()
val seriesPublishedDay = Expressions.enumPath(SeriesPublishedDaysOfWeek::class.java, "seriesPublishedDay")
val publishedDaysConcat = Expressions.stringTemplate(
"group_concat(distinct {0} order by {0} separator ',')",
seriesPublishedDay
)
val results = queryFactory
.select(
series.id,
series.title,
seriesTranslation.renderedPayload,
series.coverImage,
series.state,
member.id,
member.nickname,
member.profileImage,
contentCount,
isNewFlag,
publishedDaysConcat
)
.from(series)
.innerJoin(series.member, member) .innerJoin(series.member, member)
.innerJoin(seriesContent).on(series.id.eq(seriesContent.series.id)) .innerJoin(seriesContent).on(series.id.eq(seriesContent.series.id))
.innerJoin(seriesContent.content, audioContent) .innerJoin(seriesContent.content, audioContent)
.leftJoin(series.publishedDaysOfWeek, seriesPublishedDay)
.leftJoin(seriesTranslation).on(series.id.eq(seriesTranslation.seriesId), seriesTranslation.locale.eq(locale))
.where(where) .where(where)
.groupBy(series.id) .groupBy(series.id)
.having(contentCount.gt(0))
.orderBy(*orderBy.toTypedArray()) .orderBy(*orderBy.toTypedArray())
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
.fetch() .fetch()
return results.map { row ->
val sId = row.get(series.id)!!
val originTitle = row.get(series.title)!!
val payload = row.get(seriesTranslation.renderedPayload)
val translatedTitle = payload?.title
val coverImg = row.get(series.coverImage)
val state = row.get(series.state)
val cId = row.get(member.id)!!
val nick = row.get(member.nickname)!!
val profImg = row.get(member.profileImage)
val nContent = row.get(contentCount) ?: 0L
val isN = (row.get(isNewFlag) ?: 0) > 0
val rawDays = parsePublishedDaysOfWeek(row.get(publishedDaysConcat))
GetSeriesListResponse.SeriesListItem(
seriesId = sId,
title = if (translatedTitle.isNullOrBlank()) originTitle else translatedTitle,
coverImage = "$imageHost/$coverImg",
publishedDaysOfWeek = "", // Service layer will fill this
isComplete = state == SeriesState.COMPLETE,
creator = GetSeriesListResponse.SeriesListItemCreator(
creatorId = cId,
nickname = nick,
profileImage = "$imageHost/$profImg"
),
numberOfContent = nContent.toInt(),
isNew = isN,
rawPublishedDaysOfWeek = rawDays
)
}
} }
override fun getSeriesByGenreTotalCount( override fun getSeriesByGenreTotalCount(
genreId: Long, genreId: Long,
isAuth: Boolean, isAuth: Boolean,
contentType: ContentType contentType: ContentType,
memberId: Long?
): Int { ): Int {
var where = series.isActive.isTrue var where = series.isActive.isTrue
.and(series.genre.id.eq(genreId)) .and(series.genre.id.eq(genreId))
.and(audioContent.isActive.isTrue)
if (!isAuth) { if (!isAuth) {
where = where.and(series.isAdult.isFalse) where = where
.and(series.isAdult.isFalse)
.and(audioContent.isAdult.isFalse)
} else { } else {
if (contentType != ContentType.ALL) { if (contentType != ContentType.ALL) {
where = where.and( where = where.and(
@@ -226,29 +370,44 @@ class ContentSeriesQueryRepositoryImpl(
} }
} }
return queryFactory if (memberId != null) {
.select(series.id) val blockedSubquery = queryFactory
.select(blockMember.id)
.from(blockMember)
.where(
blockMember.member.id.eq(series.member.id),
blockMember.blockedMember.id.eq(memberId),
blockMember.isActive.isTrue
)
where = where.and(blockedSubquery.exists().not())
}
return (
queryFactory
.select(series.id.countDistinct())
.from(seriesContent) .from(seriesContent)
.innerJoin(seriesContent.series, series) .innerJoin(seriesContent.series, series)
.innerJoin(seriesContent.content, audioContent) .innerJoin(seriesContent.content, audioContent)
.innerJoin(series.member, member) .innerJoin(series.member, member)
.innerJoin(series.genre, seriesGenre) .innerJoin(series.genre, seriesGenre)
.where(where) .where(where)
.groupBy(series.id) .fetchOne() ?: 0L
.fetch() ).toInt()
.size
} }
override fun getSeriesByGenreList( override fun getSeriesByGenreListV2(
imageHost: String, imageHost: String,
genreId: Long, genreId: Long,
isAuth: Boolean, isAuth: Boolean,
contentType: ContentType, contentType: ContentType,
offset: Long, offset: Long,
limit: Long limit: Long,
): List<Series> { locale: String,
memberId: Long?
): List<GetSeriesListResponse.SeriesListItem> {
var where = series.isActive.isTrue var where = series.isActive.isTrue
.and(series.genre.id.eq(genreId)) .and(series.genre.id.eq(genreId))
.and(audioContent.isActive.isTrue)
if (!isAuth) { if (!isAuth) {
where = where.and(series.isAdult.isFalse) where = where.and(series.isAdult.isFalse)
@@ -257,30 +416,106 @@ class ContentSeriesQueryRepositoryImpl(
where = where.and( where = where.and(
series.member.isNull.or( series.member.isNull.or(
series.member.auth.gender.eq( series.member.auth.gender.eq(
if (contentType == ContentType.MALE) { if (contentType == ContentType.MALE) 0 else 1
0
} else {
1
}
) )
) )
) )
} }
} }
return queryFactory if (!isAuth) {
.select(series) where = where.and(audioContent.isAdult.isFalse)
}
// 차단 필터
if (memberId != null) {
val blockedSubquery = queryFactory
.select(blockMember.id)
.from(blockMember)
.where(
blockMember.member.id.eq(series.member.id),
blockMember.blockedMember.id.eq(memberId),
blockMember.isActive.isTrue
)
where = where.and(blockedSubquery.exists().not())
}
val now = LocalDateTime.now()
val sevenDaysAgo = now.minusDays(7)
val contentCount = seriesContent.id.countDistinct()
val isNewCase = Expressions.numberTemplate(
Int::class.java,
"case when {0} then 1 else 0 end",
audioContent.releaseDate.between(sevenDaysAgo, now)
)
val isNewFlag = isNewCase.max()
val seriesPublishedDay = Expressions.enumPath(SeriesPublishedDaysOfWeek::class.java, "seriesPublishedDay")
val publishedDaysConcat = Expressions.stringTemplate(
"group_concat(distinct {0} order by {0} separator ',')",
seriesPublishedDay
)
val latestReleaseDate = audioContent.releaseDate.max()
val results = queryFactory
.select(
series.id,
series.title,
seriesTranslation.renderedPayload,
series.coverImage,
series.state,
member.id,
member.nickname,
member.profileImage,
contentCount,
isNewFlag,
publishedDaysConcat
)
.from(seriesContent) .from(seriesContent)
.innerJoin(seriesContent.series, series) .innerJoin(seriesContent.series, series)
.innerJoin(seriesContent.content, audioContent) .innerJoin(seriesContent.content, audioContent)
.innerJoin(series.member, member) .innerJoin(series.member, member)
.innerJoin(series.genre, seriesGenre) .innerJoin(series.genre, seriesGenre)
.leftJoin(series.publishedDaysOfWeek, seriesPublishedDay)
.leftJoin(seriesTranslation).on(series.id.eq(seriesTranslation.seriesId), seriesTranslation.locale.eq(locale))
.where(where) .where(where)
.groupBy(series.id) .groupBy(series.id)
.orderBy(audioContent.releaseDate.max().desc(), series.createdAt.asc()) .having(contentCount.gt(0))
.orderBy(latestReleaseDate.desc(), series.createdAt.asc())
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
.fetch() .fetch()
return results.map { row ->
val sId = row.get(series.id)!!
val originTitle = row.get(series.title)!!
val payload = row.get(seriesTranslation.renderedPayload)
val translatedTitle = payload?.title
val coverImg = row.get(series.coverImage)
val state = row.get(series.state)
val cId = row.get(member.id)!!
val nick = row.get(member.nickname)!!
val profImg = row.get(member.profileImage)
val nContent = row.get(contentCount) ?: 0L
val isN = (row.get(isNewFlag) ?: 0) > 0
val rawDays = parsePublishedDaysOfWeek(row.get(publishedDaysConcat))
GetSeriesListResponse.SeriesListItem(
seriesId = sId,
title = if (translatedTitle.isNullOrBlank()) originTitle else translatedTitle,
coverImage = "$imageHost/$coverImg",
publishedDaysOfWeek = "", // Service layer will fill this
isComplete = state == SeriesState.COMPLETE,
creator = GetSeriesListResponse.SeriesListItemCreator(
creatorId = cId,
nickname = nick,
profileImage = "$imageHost/$profImg"
),
numberOfContent = nContent.toInt(),
isNew = isN,
rawPublishedDaysOfWeek = rawDays
)
}
} }
override fun getSeriesDetail(seriesId: Long, isAuth: Boolean, contentType: ContentType): Series? { override fun getSeriesDetail(seriesId: Long, isAuth: Boolean, contentType: ContentType): Series? {
@@ -337,34 +572,120 @@ class ContentSeriesQueryRepositoryImpl(
.fetchFirst() .fetchFirst()
} }
override fun getRecommendSeriesList(isAuth: Boolean, contentType: ContentType, limit: Long): List<Series> { override fun getRecommendSeriesListV2(
imageHost: String,
isAuth: Boolean,
contentType: ContentType,
limit: Long,
locale: String,
memberId: Long?
): List<GetSeriesListResponse.SeriesListItem> {
var where = series.isActive.isTrue var where = series.isActive.isTrue
.and(audioContent.isActive.isTrue)
if (!isAuth) { if (!isAuth) {
where = where.and(series.isAdult.isFalse) where = where
.and(series.isAdult.isFalse)
.and(audioContent.isAdult.isFalse)
} else { } else {
if (contentType != ContentType.ALL) { if (contentType != ContentType.ALL) {
where = where.and( where = where.and(
series.member.isNull.or( series.member.isNull.or(
series.member.auth.gender.eq( series.member.auth.gender.eq(
if (contentType == ContentType.MALE) { if (contentType == ContentType.MALE) 0 else 1
0
} else {
1
}
) )
) )
) )
} }
} }
return queryFactory // 차단 필터
.selectFrom(series) if (memberId != null) {
val blockedSubquery = queryFactory
.select(blockMember.id)
.from(blockMember)
.where(
blockMember.member.id.eq(series.member.id),
blockMember.blockedMember.id.eq(memberId),
blockMember.isActive.isTrue
)
where = where.and(blockedSubquery.exists().not())
}
val now = LocalDateTime.now()
val sevenDaysAgo = now.minusDays(7)
val contentCount = seriesContent.id.countDistinct()
val isNewCase = Expressions.numberTemplate(
Int::class.java,
"case when {0} then 1 else 0 end",
audioContent.releaseDate.between(sevenDaysAgo, now)
)
val isNewFlag = isNewCase.max()
val seriesPublishedDay = Expressions.enumPath(SeriesPublishedDaysOfWeek::class.java, "seriesPublishedDay")
val publishedDaysConcat = Expressions.stringTemplate(
"group_concat(distinct {0} order by {0} separator ',')",
seriesPublishedDay
)
val results = queryFactory
.select(
series.id,
series.title,
seriesTranslation.renderedPayload,
series.coverImage,
series.state,
member.id,
member.nickname,
member.profileImage,
contentCount,
isNewFlag,
publishedDaysConcat
)
.from(series)
.innerJoin(series.member, member)
.innerJoin(seriesContent).on(series.id.eq(seriesContent.series.id))
.innerJoin(seriesContent.content, audioContent)
.leftJoin(series.publishedDaysOfWeek, seriesPublishedDay)
.leftJoin(seriesTranslation).on(series.id.eq(seriesTranslation.seriesId), seriesTranslation.locale.eq(locale))
.where(where) .where(where)
.groupBy(series.id)
.having(contentCount.gt(0))
.orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc()) .orderBy(Expressions.numberTemplate(Double::class.java, "function('rand')").asc())
.offset(0) .offset(0)
.limit(limit) .limit(limit)
.fetch() .fetch()
return results.map { row ->
val sId = row.get(series.id)!!
val originTitle = row.get(series.title)!!
val payload = row.get(seriesTranslation.renderedPayload)
val translatedTitle = payload?.title
val coverImg = row.get(series.coverImage)
val state = row.get(series.state)
val cId = row.get(member.id)!!
val nick = row.get(member.nickname)!!
val profImg = row.get(member.profileImage)
val nContent = row.get(contentCount) ?: 0L
val isN = (row.get(isNewFlag) ?: 0) > 0
val rawDays = parsePublishedDaysOfWeek(row.get(publishedDaysConcat))
GetSeriesListResponse.SeriesListItem(
seriesId = sId,
title = if (translatedTitle.isNullOrBlank()) originTitle else translatedTitle,
coverImage = "$imageHost/$coverImg",
publishedDaysOfWeek = "", // Service layer will fill this
isComplete = state == SeriesState.COMPLETE,
creator = GetSeriesListResponse.SeriesListItemCreator(
creatorId = cId,
nickname = nick,
profileImage = "$imageHost/$profImg"
),
numberOfContent = nContent.toInt(),
isNew = isN,
rawPublishedDaysOfWeek = rawDays
)
}
} }
override fun getOriginalAudioDramaList( override fun getOriginalAudioDramaList(
@@ -377,6 +698,7 @@ class ContentSeriesQueryRepositoryImpl(
): List<GetSeriesListResponse.SeriesListItem> { ): List<GetSeriesListResponse.SeriesListItem> {
var where = series.isOriginal.isTrue var where = series.isOriginal.isTrue
.and(series.isActive.isTrue) .and(series.isActive.isTrue)
.and(audioContent.isActive.isTrue)
if (!isAdult) { if (!isAdult) {
where = where.and(series.isAdult.isFalse) where = where.and(series.isAdult.isFalse)
@@ -396,30 +718,25 @@ class ContentSeriesQueryRepositoryImpl(
} }
} }
if (!isAdult) {
where = where.and(audioContent.isAdult.isFalse)
}
val now = LocalDateTime.now() val now = LocalDateTime.now()
val sevenDaysAgo = now.minusDays(7) val sevenDaysAgo = now.minusDays(7)
val contentCountSubquery = queryFactory val contentCount = seriesContent.id.countDistinct()
.select(seriesContent.id.count()) val isNewCase = Expressions.numberTemplate(
.from(seriesContent) Int::class.java,
.innerJoin(seriesContent.content, audioContent) "case when {0} then 1 else 0 end",
.where(
seriesContent.series.id.eq(series.id),
audioContent.isActive.isTrue,
if (!isAdult) audioContent.isAdult.isFalse else null
)
val isNewSubquery = queryFactory
.select(seriesContent.id)
.from(seriesContent)
.innerJoin(seriesContent.content, audioContent)
.where(
seriesContent.series.id.eq(series.id),
audioContent.isActive.isTrue,
if (!isAdult) audioContent.isAdult.isFalse else null,
audioContent.releaseDate.between(sevenDaysAgo, now) audioContent.releaseDate.between(sevenDaysAgo, now)
) )
.limit(1) val isNewFlag = isNewCase.max()
val seriesPublishedDay = Expressions.enumPath(SeriesPublishedDaysOfWeek::class.java, "seriesPublishedDay")
val publishedDaysConcat = Expressions.stringTemplate(
"group_concat(distinct {0} order by {0} separator ',')",
seriesPublishedDay
)
val results = queryFactory val results = queryFactory
.select( .select(
@@ -431,15 +748,19 @@ class ContentSeriesQueryRepositoryImpl(
member.id, member.id,
member.nickname, member.nickname,
member.profileImage, member.profileImage,
contentCountSubquery, contentCount,
isNewSubquery.exists(), isNewFlag,
series publishedDaysConcat
) )
.from(series) .from(series)
.innerJoin(series.member, member) .innerJoin(series.member, member)
.innerJoin(seriesContent).on(series.id.eq(seriesContent.series.id))
.innerJoin(seriesContent.content, audioContent)
.leftJoin(series.publishedDaysOfWeek, seriesPublishedDay)
.leftJoin(seriesTranslation).on(series.id.eq(seriesTranslation.seriesId), seriesTranslation.locale.eq(locale)) .leftJoin(seriesTranslation).on(series.id.eq(seriesTranslation.seriesId), seriesTranslation.locale.eq(locale))
.where(where) .where(where)
.having(contentCountSubquery.gt(0)) .groupBy(series.id)
.having(contentCount.gt(0))
.orderBy(series.id.desc()) .orderBy(series.id.desc())
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
@@ -455,9 +776,9 @@ class ContentSeriesQueryRepositoryImpl(
val creatorId = row.get(member.id)!! val creatorId = row.get(member.id)!!
val nickname = row.get(member.nickname)!! val nickname = row.get(member.nickname)!!
val profileImage = row.get(member.profileImage) val profileImage = row.get(member.profileImage)
val numberOfContent = row.get(8, Long::class.java) ?: 0L val numberOfContent = row.get(contentCount) ?: 0L
val isNew = row.get(9, Boolean::class.java) ?: false val isNew = (row.get(isNewFlag) ?: 0) > 0
val seriesEntity = row.get(series)!! val rawDays = parsePublishedDaysOfWeek(row.get(publishedDaysConcat))
GetSeriesListResponse.SeriesListItem( GetSeriesListResponse.SeriesListItem(
seriesId = seriesId, seriesId = seriesId,
@@ -472,7 +793,7 @@ class ContentSeriesQueryRepositoryImpl(
), ),
numberOfContent = numberOfContent.toInt(), numberOfContent = numberOfContent.toInt(),
isNew = isNew, isNew = isNew,
rawPublishedDaysOfWeek = seriesEntity.publishedDaysOfWeek rawPublishedDaysOfWeek = rawDays
) )
} }
} }
@@ -560,12 +881,14 @@ class ContentSeriesQueryRepositoryImpl(
.fetch() .fetch()
} }
override fun findByCurationId( override fun findByCurationIdV2(
imageHost: String,
curationId: Long, curationId: Long,
memberId: Long, memberId: Long,
isAdult: Boolean, isAdult: Boolean,
contentType: ContentType contentType: ContentType,
): List<Series> { locale: String
): List<GetSeriesListResponse.SeriesListItem> {
val blockMemberCondition = blockMember.member.id.eq(member.id) val blockMemberCondition = blockMember.member.id.eq(member.id)
.and(blockMember.isActive.isTrue) .and(blockMember.isActive.isTrue)
.and(blockMember.blockedMember.id.eq(memberId)) .and(blockMember.blockedMember.id.eq(memberId))
@@ -576,6 +899,7 @@ class ContentSeriesQueryRepositoryImpl(
.and(audioContentCuration.id.eq(curationId)) .and(audioContentCuration.id.eq(curationId))
.and(audioContentCurationItem.isActive.isTrue) .and(audioContentCurationItem.isActive.isTrue)
.and(blockMember.id.isNull) .and(blockMember.id.isNull)
.and(audioContent.isActive.isTrue)
if (!isAdult) { if (!isAdult) {
where = where.and(series.isAdult.isFalse) where = where.and(series.isAdult.isFalse)
@@ -584,38 +908,108 @@ class ContentSeriesQueryRepositoryImpl(
where = where.and( where = where.and(
series.member.isNull.or( series.member.isNull.or(
series.member.auth.gender.eq( series.member.auth.gender.eq(
if (contentType == ContentType.MALE) { if (contentType == ContentType.MALE) 0 else 1
0
} else {
1
}
) )
) )
) )
} }
} }
return queryFactory if (!isAdult) {
.select(series) where = where.and(audioContent.isAdult.isFalse)
}
val now = LocalDateTime.now()
val sevenDaysAgo = now.minusDays(7)
val contentCount = seriesContent.id.countDistinct()
val isNewCase = Expressions.numberTemplate(
Int::class.java,
"case when {0} then 1 else 0 end",
audioContent.releaseDate.between(sevenDaysAgo, now)
)
val isNewFlag = isNewCase.max()
val minCurationOrder = audioContentCurationItem.orders.min()
val seriesPublishedDay = Expressions.enumPath(SeriesPublishedDaysOfWeek::class.java, "seriesPublishedDay")
val publishedDaysConcat = Expressions.stringTemplate(
"group_concat(distinct {0} order by {0} separator ',')",
seriesPublishedDay
)
val results = queryFactory
.select(
series.id,
series.title,
seriesTranslation.renderedPayload,
series.coverImage,
series.state,
member.id,
member.nickname,
member.profileImage,
contentCount,
isNewFlag,
publishedDaysConcat,
minCurationOrder
)
.from(audioContentCurationItem) .from(audioContentCurationItem)
.innerJoin(audioContentCurationItem.curation, audioContentCuration) .innerJoin(audioContentCurationItem.curation, audioContentCuration)
.innerJoin(audioContentCurationItem.series, series) .innerJoin(audioContentCurationItem.series, series)
.innerJoin(series.member, member) .innerJoin(series.member, member)
.innerJoin(seriesContent).on(series.id.eq(seriesContent.series.id))
.innerJoin(seriesContent.content, audioContent)
.leftJoin(series.publishedDaysOfWeek, seriesPublishedDay)
.leftJoin(blockMember).on(blockMemberCondition) .leftJoin(blockMember).on(blockMemberCondition)
.leftJoin(seriesTranslation).on(series.id.eq(seriesTranslation.seriesId), seriesTranslation.locale.eq(locale))
.where(where) .where(where)
.orderBy(audioContentCurationItem.orders.asc()) .groupBy(series.id)
.orderBy(minCurationOrder.asc(), series.id.asc())
.fetch() .fetch()
return results.map { row ->
val sId = row.get(series.id)!!
val originTitle = row.get(series.title)!!
val payload = row.get(seriesTranslation.renderedPayload)
val translatedTitle = payload?.title
val coverImg = row.get(series.coverImage)
val state = row.get(series.state)
val cId = row.get(member.id)!!
val nick = row.get(member.nickname)!!
val profImg = row.get(member.profileImage)
val nContent = row.get(contentCount) ?: 0L
val isN = (row.get(isNewFlag) ?: 0) > 0
val rawDays = parsePublishedDaysOfWeek(row.get(publishedDaysConcat))
GetSeriesListResponse.SeriesListItem(
seriesId = sId,
title = if (translatedTitle.isNullOrBlank()) originTitle else translatedTitle,
coverImage = "$imageHost/$coverImg",
publishedDaysOfWeek = "", // Service layer will fill this
isComplete = state == SeriesState.COMPLETE,
creator = GetSeriesListResponse.SeriesListItemCreator(
creatorId = cId,
nickname = nick,
profileImage = "$imageHost/$profImg"
),
numberOfContent = nContent.toInt(),
isNew = isN,
rawPublishedDaysOfWeek = rawDays
)
}
} }
override fun getDayOfWeekSeriesList( override fun getDayOfWeekSeriesListV2(
imageHost: String,
dayOfWeek: SeriesPublishedDaysOfWeek, dayOfWeek: SeriesPublishedDaysOfWeek,
contentType: ContentType, contentType: ContentType,
isAdult: Boolean, isAdult: Boolean,
offset: Long, offset: Long,
limit: Long limit: Long,
): List<Series> { locale: String,
memberId: Long?
): List<GetSeriesListResponse.SeriesListItem> {
var where = series.isActive.isTrue var where = series.isActive.isTrue
.and(series.publishedDaysOfWeek.contains(dayOfWeek)) .and(series.publishedDaysOfWeek.contains(dayOfWeek))
.and(audioContent.isActive.isTrue)
if (!isAdult) { if (!isAdult) {
where = where.and(series.isAdult.isFalse) where = where.and(series.isAdult.isFalse)
@@ -624,24 +1018,103 @@ class ContentSeriesQueryRepositoryImpl(
where = where.and( where = where.and(
series.member.isNull.or( series.member.isNull.or(
series.member.auth.gender.eq( series.member.auth.gender.eq(
if (contentType == ContentType.MALE) { if (contentType == ContentType.MALE) 0 else 1
0
} else {
1
}
) )
) )
) )
} }
} }
return queryFactory if (!isAdult) {
.selectFrom(series) where = where.and(audioContent.isAdult.isFalse)
}
// 차단 필터
if (memberId != null) {
val blockedSubquery = queryFactory
.select(blockMember.id)
.from(blockMember)
.where(
blockMember.member.id.eq(series.member.id),
blockMember.blockedMember.id.eq(memberId),
blockMember.isActive.isTrue
)
where = where.and(blockedSubquery.exists().not())
}
val now = LocalDateTime.now()
val sevenDaysAgo = now.minusDays(7)
val contentCount = seriesContent.id.countDistinct()
val isNewCase = Expressions.numberTemplate(
Int::class.java,
"case when {0} then 1 else 0 end",
audioContent.releaseDate.between(sevenDaysAgo, now)
)
val isNewFlag = isNewCase.max()
val seriesPublishedDay = Expressions.enumPath(SeriesPublishedDaysOfWeek::class.java, "seriesPublishedDay")
val publishedDaysConcat = Expressions.stringTemplate(
"group_concat(distinct {0} order by {0} separator ',')",
seriesPublishedDay
)
val results = queryFactory
.select(
series.id,
series.title,
seriesTranslation.renderedPayload,
series.coverImage,
series.state,
member.id,
member.nickname,
member.profileImage,
contentCount,
isNewFlag,
publishedDaysConcat
)
.from(series)
.innerJoin(series.member, member)
.innerJoin(seriesContent).on(series.id.eq(seriesContent.series.id))
.innerJoin(seriesContent.content, audioContent)
.leftJoin(series.publishedDaysOfWeek, seriesPublishedDay)
.leftJoin(seriesTranslation).on(series.id.eq(seriesTranslation.seriesId), seriesTranslation.locale.eq(locale))
.where(where) .where(where)
.groupBy(series.id) .groupBy(series.id)
.having(contentCount.gt(0))
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
.orderBy(series.createdAt.desc()) .orderBy(series.createdAt.desc())
.fetch() .fetch()
return results.map { row ->
val sId = row.get(series.id)!!
val originTitle = row.get(series.title)!!
val payload = row.get(seriesTranslation.renderedPayload)
val translatedTitle = payload?.title
val coverImg = row.get(series.coverImage)
val state = row.get(series.state)
val cId = row.get(member.id)!!
val nick = row.get(member.nickname)!!
val profImg = row.get(member.profileImage)
val nContent = row.get(contentCount) ?: 0L
val isN = (row.get(isNewFlag) ?: 0) > 0
val rawDays = parsePublishedDaysOfWeek(row.get(publishedDaysConcat))
GetSeriesListResponse.SeriesListItem(
seriesId = sId,
title = if (translatedTitle.isNullOrBlank()) originTitle else translatedTitle,
coverImage = "$imageHost/$coverImg",
publishedDaysOfWeek = "", // Service layer will fill this
isComplete = state == SeriesState.COMPLETE,
creator = GetSeriesListResponse.SeriesListItemCreator(
creatorId = cId,
nickname = nick,
profileImage = "$imageHost/$profImg"
),
numberOfContent = nContent.toInt(),
isNew = isN,
rawPublishedDaysOfWeek = rawDays
)
}
} }
} }

View File

@@ -12,10 +12,8 @@ import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayl
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationRepository
import kr.co.vividnext.sodalive.content.series.translation.TranslatedSeries import kr.co.vividnext.sodalive.content.series.translation.TranslatedSeries
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
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.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesSortType
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.LangContext
@@ -26,7 +24,6 @@ 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.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@@ -160,10 +157,11 @@ class ContentSeriesService(
isAuth = isAuth, isAuth = isAuth,
contentType = contentType, contentType = contentType,
isOriginal = isOriginal, isOriginal = isOriginal,
isCompleted = isCompleted isCompleted = isCompleted,
memberId = member.id
) )
val rawItems = repository.getSeriesList( val items = repository.getSeriesListV2(
imageHost = coverImageHost, imageHost = coverImageHost,
creatorId = creatorId, creatorId = creatorId,
isAuth = isAuth, isAuth = isAuth,
@@ -172,11 +170,14 @@ class ContentSeriesService(
isCompleted = isCompleted, isCompleted = isCompleted,
orderByRandom = orderByRandom, orderByRandom = orderByRandom,
offset = offset, offset = offset,
limit = limit limit = limit,
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) } locale = langContext.lang.code,
memberId = member.id
).map { item ->
item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
}
val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType) return GetSeriesListResponse(totalCount, items)
return GetSeriesListResponse(totalCount, getTranslatedSeriesList(items))
} }
fun getSeriesListByGenre( fun getSeriesListByGenre(
@@ -192,20 +193,24 @@ class ContentSeriesService(
val totalCount = repository.getSeriesByGenreTotalCount( val totalCount = repository.getSeriesByGenreTotalCount(
genreId = genreId, genreId = genreId,
isAuth = isAuth, isAuth = isAuth,
contentType = contentType contentType = contentType,
memberId = member.id
) )
val rawItems = repository.getSeriesByGenreList( val items = repository.getSeriesByGenreListV2(
imageHost = coverImageHost, imageHost = coverImageHost,
genreId = genreId, genreId = genreId,
isAuth = isAuth, isAuth = isAuth,
contentType = contentType, contentType = contentType,
offset = offset, offset = offset,
limit = limit limit = limit,
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) } locale = langContext.lang.code,
memberId = member.id
).map { item ->
item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
}
val items = seriesToSeriesListItem(seriesList = rawItems, isAdult = isAuth, contentType = contentType) return GetSeriesListResponse(totalCount, items)
return GetSeriesListResponse(totalCount, getTranslatedSeriesList(items))
} }
@Transactional @Transactional
@@ -468,19 +473,16 @@ class ContentSeriesService(
member: Member member: Member
): List<GetSeriesListResponse.SeriesListItem> { ): List<GetSeriesListResponse.SeriesListItem> {
val isAuth = member.auth != null && isAdultContentVisible val isAuth = member.auth != null && isAdultContentVisible
val seriesList = repository.getRecommendSeriesList( return repository.getRecommendSeriesListV2(
imageHost = coverImageHost,
isAuth = isAuth, isAuth = isAuth,
contentType = contentType, contentType = contentType,
limit = 20 limit = 20,
).filter { !blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!) } locale = langContext.lang.code,
memberId = member.id
return getTranslatedSeriesList( ).map { item ->
seriesToSeriesListItem( item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
seriesList = seriesList, }
isAdult = isAuth,
contentType = contentType
)
)
} }
fun fetchSeriesByCurationId( fun fetchSeriesByCurationId(
@@ -489,13 +491,16 @@ class ContentSeriesService(
isAdult: Boolean, isAdult: Boolean,
contentType: ContentType contentType: ContentType
): List<GetSeriesListResponse.SeriesListItem> { ): List<GetSeriesListResponse.SeriesListItem> {
val seriesList = repository.findByCurationId( return repository.findByCurationIdV2(
imageHost = coverImageHost,
curationId = curationId, curationId = curationId,
memberId = memberId, memberId = memberId,
isAdult = isAdult, isAdult = isAdult,
contentType = contentType contentType = contentType,
) locale = langContext.lang.code
return getTranslatedSeriesList(seriesToSeriesListItem(seriesList, isAdult, contentType)) ).map { item ->
item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
}
} }
fun getDayOfWeekSeriesList( fun getDayOfWeekSeriesList(
@@ -506,71 +511,17 @@ class ContentSeriesService(
offset: Long = 0, offset: Long = 0,
limit: Long = 10 limit: Long = 10
): List<GetSeriesListResponse.SeriesListItem> { ): List<GetSeriesListResponse.SeriesListItem> {
var seriesList = repository.getDayOfWeekSeriesList( return repository.getDayOfWeekSeriesListV2(
imageHost = coverImageHost,
dayOfWeek = dayOfWeek, dayOfWeek = dayOfWeek,
contentType = contentType, contentType = contentType,
isAdult = isAdult, isAdult = isAdult,
offset = offset, offset = offset,
limit = limit limit = limit,
) locale = langContext.lang.code,
memberId = memberId
seriesList = if (memberId != null) { ).map { item ->
seriesList.filter { item.copy(publishedDaysOfWeek = publishedDaysOfWeekText(item.rawPublishedDaysOfWeek))
!blockMemberRepository.isBlocked(
blockedMemberId = memberId,
memberId = it.member!!.id!!
)
}
} else {
seriesList
}
return getTranslatedSeriesList(seriesToSeriesListItem(seriesList, isAdult, contentType))
}
private fun seriesToSeriesListItem(
seriesList: List<Series>,
isAdult: Boolean,
contentType: ContentType
): List<GetSeriesListResponse.SeriesListItem> {
return seriesList
.map {
GetSeriesListResponse.SeriesListItem(
seriesId = it.id!!,
title = it.title,
coverImage = "$coverImageHost/${it.coverImage!!}",
publishedDaysOfWeek = publishedDaysOfWeekText(it.publishedDaysOfWeek),
isComplete = it.state == SeriesState.COMPLETE,
creator = GetSeriesListResponse.SeriesListItemCreator(
creatorId = it.member!!.id!!,
nickname = it.member!!.nickname,
profileImage = "$coverImageHost/${it.member!!.profileImage!!}"
)
)
}
.map {
it.numberOfContent = seriesContentRepository.getContentCount(
seriesId = it.seriesId,
isAdult = isAdult,
contentType = contentType
)
it
}
.filter {
it.numberOfContent > 0
}
.map {
val nowDateTime = LocalDateTime.now()
it.isNew = seriesContentRepository.isNewContent(
seriesId = it.seriesId,
isAdult = isAdult,
fromDate = nowDateTime.minusDays(7),
nowDate = nowDateTime
)
it
} }
} }
@@ -641,39 +592,4 @@ class ContentSeriesService(
} }
} }
} }
/**
* 시리즈 리스트의 제목을 현재 언어(locale)에 맞춰 일괄 번역한다.
*
* 처리 절차:
* - 입력된 시리즈들의 seriesId 집합을 만들고, 요청 언어 코드(langContext.lang.code)로
* seriesTranslationRepository에서 번역 데이터를 한 번에 조회한다.
* - 각 항목에 대해 번역된 제목이 존재하고 비어있지 않으면 title만 번역 값으로 교체한다.
* - 번역이 없거나 공백이면 원본 항목을 그대로 반환한다.
*
* 성능:
* - N건의 항목을 1회의 조회로 해결하기 위해 IN 쿼리를 사용한다.
*
* @param seriesList 번역 대상 SeriesListItem 목록
* @return 제목이 가능한 항목은 번역된 목록(불변 사본), 그 외는 원본 항목 유지
*/
private fun getTranslatedSeriesList(
seriesList: List<GetSeriesListResponse.SeriesListItem>
): List<GetSeriesListResponse.SeriesListItem> {
val seriesIds = seriesList.map { it.seriesId }
if (seriesIds.isEmpty()) return seriesList
val translations = seriesTranslationRepository
.findBySeriesIdInAndLocale(seriesIds = seriesIds, locale = langContext.lang.code)
.associateBy { it.seriesId }
return seriesList.map { item ->
val translatedTitle = translations[item.seriesId]?.renderedPayload?.title
if (translatedTitle.isNullOrBlank()) {
item
} else {
item.copy(title = translatedTitle)
}
}
}
} }