feat(chat-character): Character DTO에 isNew 매핑 적용(N+1 제거)
- 내용: 서비스 매핑에서 보조 쿼리 결과를 이용해 `isNew` 채움
This commit is contained in:
@@ -92,7 +92,8 @@ class ChatCharacterController(
|
||||
characterId = it.id!!,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
|
||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}",
|
||||
isNew = false
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -21,7 +21,8 @@ data class Character(
|
||||
@JsonProperty("characterId") val characterId: Long,
|
||||
@JsonProperty("name") val name: String,
|
||||
@JsonProperty("description") val description: String,
|
||||
@JsonProperty("imageUrl") val imageUrl: String
|
||||
@JsonProperty("imageUrl") val imageUrl: String,
|
||||
@JsonProperty("isNew") val isNew: Boolean
|
||||
)
|
||||
|
||||
data class RecentCharacter(
|
||||
|
||||
@@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.chat.character.ChatCharacterTag
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacterValue
|
||||
import kr.co.vividnext.sodalive.chat.character.dto.Character
|
||||
import kr.co.vividnext.sodalive.chat.character.dto.RecentCharactersResponse
|
||||
import kr.co.vividnext.sodalive.chat.character.image.CharacterImageRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterGoalRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterHobbyRepository
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||
@@ -34,6 +35,7 @@ class ChatCharacterService(
|
||||
private val hobbyRepository: ChatCharacterHobbyRepository,
|
||||
private val goalRepository: ChatCharacterGoalRepository,
|
||||
private val popularCharacterQuery: PopularCharacterQuery,
|
||||
private val imageRepository: CharacterImageRepository,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val imageHost: String
|
||||
@@ -46,12 +48,25 @@ class ChatCharacterService(
|
||||
} else {
|
||||
chatCharacterRepository.findRandomActive(PageRequest.of(0, safeLimit))
|
||||
}
|
||||
|
||||
val recentSet = if (chars.isNotEmpty()) {
|
||||
imageRepository
|
||||
.findCharacterIdsWithRecentImages(
|
||||
chars.map { it.id!! },
|
||||
LocalDateTime.now().minusDays(3)
|
||||
)
|
||||
.toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
|
||||
return chars.map {
|
||||
Character(
|
||||
characterId = it.id!!,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
|
||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}",
|
||||
isNew = recentSet.contains(it.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -69,12 +84,25 @@ class ChatCharacterService(
|
||||
val window = RankingWindowCalculator.now("popular-character")
|
||||
val topIds = popularCharacterQuery.findPopularCharacterIds(window.windowStart, window.nextBoundary, limit)
|
||||
val list = loadCharactersInOrder(topIds)
|
||||
|
||||
val recentSet = if (list.isNotEmpty()) {
|
||||
imageRepository
|
||||
.findCharacterIdsWithRecentImages(
|
||||
list.map { it.id!! },
|
||||
LocalDateTime.now().minusDays(3)
|
||||
)
|
||||
.toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
|
||||
return list.map {
|
||||
Character(
|
||||
characterId = it.id!!,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
|
||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}",
|
||||
isNew = recentSet.contains(it.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -109,15 +137,28 @@ class ChatCharacterService(
|
||||
content = emptyList()
|
||||
)
|
||||
}
|
||||
val fallback = chatCharacterRepository.findByIsActiveTrue(
|
||||
val chars = chatCharacterRepository.findByIsActiveTrue(
|
||||
PageRequest.of(0, 20, Sort.by("createdAt").descending())
|
||||
)
|
||||
val content = fallback.content.map {
|
||||
).content
|
||||
|
||||
val recentSet = if (chars.isNotEmpty()) {
|
||||
imageRepository
|
||||
.findCharacterIdsWithRecentImages(
|
||||
chars.map { it.id!! },
|
||||
LocalDateTime.now().minusDays(3)
|
||||
)
|
||||
.toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
|
||||
val content = chars.map {
|
||||
Character(
|
||||
characterId = it.id!!,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
|
||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}",
|
||||
isNew = recentSet.contains(it.id)
|
||||
)
|
||||
}
|
||||
return RecentCharactersResponse(
|
||||
@@ -126,16 +167,29 @@ class ChatCharacterService(
|
||||
)
|
||||
}
|
||||
|
||||
val pageResult = chatCharacterRepository.findRecentSince(
|
||||
val chars = chatCharacterRepository.findRecentSince(
|
||||
since,
|
||||
PageRequest.of(safePage, safeSize)
|
||||
)
|
||||
val content = pageResult.content.map {
|
||||
).content
|
||||
|
||||
val recentSet = if (chars.isNotEmpty()) {
|
||||
imageRepository
|
||||
.findCharacterIdsWithRecentImages(
|
||||
chars.map { it.id!! },
|
||||
LocalDateTime.now().minusDays(3)
|
||||
)
|
||||
.toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
|
||||
val content = chars.map {
|
||||
Character(
|
||||
characterId = it.id!!,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}"
|
||||
imageUrl = "$imageHost/${it.imagePath ?: "profile/default-profile.png"}",
|
||||
isNew = recentSet.contains(it.id)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.chat.original.controller
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||
import kr.co.vividnext.sodalive.chat.character.dto.Character
|
||||
import kr.co.vividnext.sodalive.chat.character.image.CharacterImageRepository
|
||||
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkDetailResponse
|
||||
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListItemResponse
|
||||
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListResponse
|
||||
@@ -15,6 +17,7 @@ import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* 앱용 원작(오리지널 작품) 공개 API
|
||||
@@ -25,6 +28,8 @@ import org.springframework.web.bind.annotation.RestController
|
||||
@RequestMapping("/api/chat/original")
|
||||
class OriginalWorkController(
|
||||
private val queryService: OriginalWorkQueryService,
|
||||
private val characterImageRepository: CharacterImageRepository,
|
||||
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val imageHost: String
|
||||
) {
|
||||
@@ -65,17 +70,34 @@ class OriginalWorkController(
|
||||
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||
|
||||
val ow = queryService.getOriginalWork(id)
|
||||
val pageRes = queryService.getActiveCharactersPage(id, page = 0, size = 20)
|
||||
val characters = pageRes.content.map {
|
||||
val path = it.imagePath ?: "profile/default-profile.png"
|
||||
Character(
|
||||
characterId = it.id!!,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
imageUrl = "$imageHost/$path"
|
||||
)
|
||||
val chars = queryService.getActiveCharactersPage(id, page = 0, size = 20).content
|
||||
|
||||
val recentSet = if (chars.isNotEmpty()) {
|
||||
characterImageRepository
|
||||
.findCharacterIdsWithRecentImages(
|
||||
chars.map { it.id!! },
|
||||
LocalDateTime.now().minusDays(3)
|
||||
)
|
||||
.toSet()
|
||||
} else {
|
||||
emptySet()
|
||||
}
|
||||
val response = OriginalWorkDetailResponse.from(ow, imageHost, characters)
|
||||
ApiResponse.ok(response)
|
||||
|
||||
ApiResponse.ok(
|
||||
OriginalWorkDetailResponse.from(
|
||||
ow,
|
||||
imageHost,
|
||||
chars.map<ChatCharacter, Character> {
|
||||
val path = it.imagePath ?: "profile/default-profile.png"
|
||||
Character(
|
||||
characterId = it.id!!,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
imageUrl = "$imageHost/$path",
|
||||
isNew = recentSet.contains(it.id)
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class OriginalWorkQueryService(
|
||||
val safePage = if (page < 0) 0 else page
|
||||
val safeSize = when {
|
||||
size <= 0 -> 20
|
||||
size > 50 -> 50
|
||||
size > 20 -> 20
|
||||
else -> size
|
||||
}
|
||||
val pageable = PageRequest.of(safePage, safeSize, Sort.by("createdAt").descending())
|
||||
|
||||
Reference in New Issue
Block a user