diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/controller/ChatCharacterController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/controller/ChatCharacterController.kt index 3957e08..055f2f3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/controller/ChatCharacterController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/controller/ChatCharacterController.kt @@ -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 ) } ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/dto/CharacterHomeResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/dto/CharacterHomeResponse.kt index 6b848d9..824be47 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/dto/CharacterHomeResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/dto/CharacterHomeResponse.kt @@ -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( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt index 68d06aa..79eeacb 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt @@ -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) ) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt index 43ad355..fab48a6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkController.kt @@ -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 { + 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) + ) + } + ) + ) } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt index b6b88f5..c32f3d3 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/original/service/OriginalWorkQueryService.kt @@ -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())