인기 캐릭터 번역 조회 개선
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
package kr.co.vividnext.sodalive.api.home
|
package kr.co.vividnext.sodalive.api.home
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.audition.AuditionService
|
import kr.co.vividnext.sodalive.audition.AuditionService
|
||||||
import kr.co.vividnext.sodalive.chat.character.dto.Character
|
|
||||||
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
import kr.co.vividnext.sodalive.chat.character.service.ChatCharacterService
|
||||||
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRepository
|
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRepository
|
||||||
import kr.co.vividnext.sodalive.content.AudioContentMainItem
|
import kr.co.vividnext.sodalive.content.AudioContentMainItem
|
||||||
@@ -145,7 +144,7 @@ class HomeService(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 인기 캐릭터 조회
|
// 인기 캐릭터 조회
|
||||||
val translatedPopularCharacters = getTranslatedAiCharacterList(aiCharacterList = characterService.getPopularCharacters())
|
val translatedPopularCharacters = characterService.getPopularCharacters(locale = langContext.lang.code)
|
||||||
|
|
||||||
val currentDateTime = LocalDateTime.now()
|
val currentDateTime = LocalDateTime.now()
|
||||||
val startDate = currentDateTime
|
val startDate = currentDateTime
|
||||||
@@ -473,38 +472,4 @@ class HomeService(
|
|||||||
|
|
||||||
return result.take(targetSize).shuffled()
|
return result.take(targetSize).shuffled()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* AI 캐릭터 리스트의 이름/설명을 현재 언어(locale)에 맞춰 일괄 번역한다.
|
|
||||||
*
|
|
||||||
* 처리 절차:
|
|
||||||
* - characterId 목록을 추출하고, 요청 언어 코드로 aiCharacterTranslationRepository에서
|
|
||||||
* 번역 데이터를 한 번에 조회한다.
|
|
||||||
* - 각 캐릭터에 대해 name과 description 모두 번역 값이 존재하고 비어있지 않을 때에만
|
|
||||||
* 해당 필드를 교체한다. 둘 중 하나라도 없으면 원본 캐릭터를 그대로 유지한다.
|
|
||||||
*
|
|
||||||
* @param aiCharacterList 번역 대상 캐릭터 목록
|
|
||||||
* @return 가능한 경우 name/description이 번역된 캐릭터 목록, 그 외는 원본 유지
|
|
||||||
*/
|
|
||||||
private fun getTranslatedAiCharacterList(aiCharacterList: List<Character>): List<Character> {
|
|
||||||
val characterIds = aiCharacterList.map { it.characterId }
|
|
||||||
|
|
||||||
return if (characterIds.isNotEmpty()) {
|
|
||||||
val translations = aiCharacterTranslationRepository
|
|
||||||
.findByCharacterIdInAndLocale(characterIds = characterIds, locale = langContext.lang.code)
|
|
||||||
.associateBy { it.characterId }
|
|
||||||
|
|
||||||
aiCharacterList.map { character ->
|
|
||||||
val translatedName = translations[character.characterId]?.renderedPayload?.name
|
|
||||||
val translatedDesc = translations[character.characterId]?.renderedPayload?.description
|
|
||||||
if (translatedName.isNullOrBlank() || translatedDesc.isNullOrBlank()) {
|
|
||||||
character
|
|
||||||
} else {
|
|
||||||
character.copy(name = translatedName, description = translatedDesc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
aiCharacterList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class ChatCharacterController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 인기 캐릭터 조회
|
// 인기 캐릭터 조회
|
||||||
val popularCharacters = service.getPopularCharacters()
|
val popularCharacters = service.getPopularCharacters(locale = langContext.lang.code)
|
||||||
|
|
||||||
// 최근 등록된 캐릭터 리스트 조회
|
// 최근 등록된 캐릭터 리스트 조회
|
||||||
val newCharacters = service.getRecentCharactersPage(
|
val newCharacters = service.getRecentCharactersPage(
|
||||||
@@ -138,7 +138,7 @@ class ChatCharacterController(
|
|||||||
CharacterMainResponse(
|
CharacterMainResponse(
|
||||||
banners = banners,
|
banners = banners,
|
||||||
recentCharacters = translatedRecentCharacters,
|
recentCharacters = translatedRecentCharacters,
|
||||||
popularCharacters = getTranslatedAiCharacterList(popularCharacters),
|
popularCharacters = popularCharacters,
|
||||||
newCharacters = getTranslatedAiCharacterList(newCharacters),
|
newCharacters = getTranslatedAiCharacterList(newCharacters),
|
||||||
recommendCharacters = getTranslatedAiCharacterList(recommendCharacters),
|
recommendCharacters = getTranslatedAiCharacterList(recommendCharacters),
|
||||||
curationSections = curationSections
|
curationSections = curationSections
|
||||||
|
|||||||
@@ -77,18 +77,23 @@ class ChatCharacterService(
|
|||||||
*/
|
*/
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@Cacheable(
|
@Cacheable(
|
||||||
cacheNames = ["popularCharacters_24h"],
|
cacheNames = ["popularCharacters_24h_locale"],
|
||||||
key = "T(kr.co.vividnext.sodalive.chat.character.service.RankingWindowCalculator).now('popular-character').cacheKey"
|
key = "T(kr.co.vividnext.sodalive.chat.character.service.RankingWindowCalculator)" +
|
||||||
|
".now('popular-character').cacheKey + '-' + #locale"
|
||||||
)
|
)
|
||||||
fun getPopularCharacters(limit: Long = 20): List<Character> {
|
fun getPopularCharacters(locale: String, limit: Long = 20): List<Character> {
|
||||||
val window = RankingWindowCalculator.now("popular-character")
|
val window = RankingWindowCalculator.now("popular-character")
|
||||||
val topIds = popularCharacterQuery.findPopularCharacterIds(window.windowStart, window.nextBoundary, limit)
|
val results = popularCharacterQuery.findPopularCharactersWithTranslation(
|
||||||
val list = loadCharactersInOrder(topIds)
|
window.windowStart,
|
||||||
|
window.nextBoundary,
|
||||||
|
limit,
|
||||||
|
locale
|
||||||
|
)
|
||||||
|
|
||||||
val recentSet = if (list.isNotEmpty()) {
|
val recentSet = if (results.isNotEmpty()) {
|
||||||
imageRepository
|
imageRepository
|
||||||
.findCharacterIdsWithRecentImages(
|
.findCharacterIdsWithRecentImages(
|
||||||
list.map { it.id!! },
|
results.map { it.id },
|
||||||
LocalDateTime.now().minusDays(3)
|
LocalDateTime.now().minusDays(3)
|
||||||
)
|
)
|
||||||
.toSet()
|
.toSet()
|
||||||
@@ -96,11 +101,11 @@ class ChatCharacterService(
|
|||||||
emptySet()
|
emptySet()
|
||||||
}
|
}
|
||||||
|
|
||||||
return list.map {
|
return results.map {
|
||||||
Character(
|
Character(
|
||||||
characterId = it.id!!,
|
characterId = it.id,
|
||||||
name = it.name,
|
name = it.translatedPayload?.name.takeIf { name -> !name.isNullOrBlank() } ?: it.name,
|
||||||
description = it.description,
|
description = it.translatedPayload?.description.takeIf { desc -> !desc.isNullOrBlank() } ?: it.description,
|
||||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}",
|
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}",
|
||||||
new = recentSet.contains(it.id)
|
new = recentSet.contains(it.id)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package kr.co.vividnext.sodalive.chat.character.service
|
package kr.co.vividnext.sodalive.chat.character.service
|
||||||
|
|
||||||
|
import com.querydsl.core.types.Projections
|
||||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
import kr.co.vividnext.sodalive.chat.character.QChatCharacter
|
import kr.co.vividnext.sodalive.chat.character.QChatCharacter
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.translate.AiCharacterTranslationRenderedPayload
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.translate.QAiCharacterTranslation
|
||||||
import kr.co.vividnext.sodalive.chat.room.ParticipantType
|
import kr.co.vividnext.sodalive.chat.room.ParticipantType
|
||||||
import kr.co.vividnext.sodalive.chat.room.QChatMessage
|
import kr.co.vividnext.sodalive.chat.room.QChatMessage
|
||||||
import kr.co.vividnext.sodalive.chat.room.QChatParticipant
|
import kr.co.vividnext.sodalive.chat.room.QChatParticipant
|
||||||
@@ -51,4 +54,56 @@ class PopularCharacterQuery(
|
|||||||
.limit(limit)
|
.limit(limit)
|
||||||
.fetch()
|
.fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class PopularCharacterQueryResult(
|
||||||
|
val id: Long,
|
||||||
|
val name: String,
|
||||||
|
val description: String,
|
||||||
|
val imagePath: String?,
|
||||||
|
val translatedPayload: AiCharacterTranslationRenderedPayload?
|
||||||
|
)
|
||||||
|
|
||||||
|
fun findPopularCharactersWithTranslation(
|
||||||
|
windowStart: Instant,
|
||||||
|
endExclusive: Instant,
|
||||||
|
limit: Long,
|
||||||
|
locale: String
|
||||||
|
): List<PopularCharacterQueryResult> {
|
||||||
|
val m = QChatMessage.chatMessage
|
||||||
|
val p = QChatParticipant.chatParticipant
|
||||||
|
val c = QChatCharacter.chatCharacter
|
||||||
|
val t = QAiCharacterTranslation.aiCharacterTranslation
|
||||||
|
|
||||||
|
val start = LocalDateTime.ofInstant(windowStart, ZoneOffset.UTC)
|
||||||
|
val end = LocalDateTime.ofInstant(endExclusive, ZoneOffset.UTC)
|
||||||
|
|
||||||
|
return queryFactory
|
||||||
|
.select(
|
||||||
|
Projections.constructor(
|
||||||
|
PopularCharacterQueryResult::class.java,
|
||||||
|
c.id,
|
||||||
|
c.name,
|
||||||
|
c.description,
|
||||||
|
c.imagePath,
|
||||||
|
t.renderedPayload
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(m)
|
||||||
|
.join(p).on(
|
||||||
|
p.chatRoom.id.eq(m.chatRoom.id)
|
||||||
|
.and(p.participantType.eq(ParticipantType.CHARACTER))
|
||||||
|
)
|
||||||
|
.join(c).on(c.id.eq(p.character.id))
|
||||||
|
.leftJoin(t).on(t.characterId.eq(c.id).and(t.locale.eq(locale)))
|
||||||
|
.where(
|
||||||
|
m.createdAt.goe(start)
|
||||||
|
.and(m.createdAt.lt(end))
|
||||||
|
.and(m.isActive.isTrue)
|
||||||
|
.and(c.isActive.isTrue)
|
||||||
|
)
|
||||||
|
.groupBy(c.id, c.name, c.description, c.imagePath, t.id, t.renderedPayload)
|
||||||
|
.orderBy(m.id.count().desc())
|
||||||
|
.limit(limit)
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user