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