feat(character-image): 캐릭터 이미지 리스트 첫 칸에 프로필 이미지 포함 및 페이징 보정
사용자 경험 향상을 위해 캐릭터 프로필 이미지를 이미지 리스트의 맨 앞에 노출하도록 변경.
This commit is contained in:
parent
48b0190242
commit
0347d767f0
|
@ -40,30 +40,73 @@ class CharacterImageController(
|
|||
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||
|
||||
val pageSize = if (size <= 0) 20 else minOf(size, 20)
|
||||
val pageable = PageRequest.of(page, pageSize)
|
||||
|
||||
val pageResult = imageService.pageActiveByCharacter(characterId, pageable)
|
||||
val totalCount = pageResult.totalElements
|
||||
|
||||
// 전체 활성 이미지 수(프로필 제외) 파악을 위해 최소 페이지 조회
|
||||
val totalActiveElements = imageService.pageActiveByCharacter(characterId, PageRequest.of(0, 1)).totalElements
|
||||
val ownedCount = imageService.countOwnedActiveByCharacterForMember(characterId, member.id!!)
|
||||
|
||||
val expiration = 5L * 60L * 1000L // 5분
|
||||
val items = pageResult.content.map { img ->
|
||||
val isOwned = (img.imagePriceCan == 0L) || imageService.isOwnedImageByMember(img.id!!, member.id!!)
|
||||
val url = if (isOwned) {
|
||||
imageCloudFront.generateSignedURL(img.imagePath, expiration)
|
||||
} else {
|
||||
"$imageHost/${img.blurImagePath}"
|
||||
}
|
||||
CharacterImageListItemResponse(
|
||||
id = img.id!!,
|
||||
imageUrl = url,
|
||||
isOwned = isOwned,
|
||||
imagePriceCan = img.imagePriceCan,
|
||||
sortOrder = img.sortOrder
|
||||
val totalCount = totalActiveElements + 1 // 프로필 포함
|
||||
|
||||
val startIndex = page * pageSize
|
||||
if (startIndex >= totalCount) {
|
||||
return@run ApiResponse.ok(
|
||||
CharacterImageListResponse(
|
||||
totalCount = totalCount,
|
||||
ownedCount = ownedCount,
|
||||
items = emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val endExclusive = kotlin.math.min(startIndex + pageSize, totalCount.toInt())
|
||||
val pageLength = endExclusive - startIndex
|
||||
|
||||
// 프로필 이미지 구성(맨 앞)
|
||||
val profilePath = imageService.getCharacterImagePath(characterId) ?: "profile/default-profile.png"
|
||||
val profileItem = CharacterImageListItemResponse(
|
||||
id = 0L,
|
||||
imageUrl = "$imageHost/$profilePath",
|
||||
isOwned = true,
|
||||
imagePriceCan = 0L,
|
||||
sortOrder = 0
|
||||
)
|
||||
|
||||
// 활성 이미지 offset/limit 계산 (결합 리스트 [프로필] + activeImages)
|
||||
val activeOffset = if (startIndex == 0) 0L else (startIndex - 1).toLong()
|
||||
val activeLimit = if (startIndex == 0) (pageLength - 1).toLong() else pageLength.toLong()
|
||||
|
||||
val expiration = 5L * 60L * 1000L // 5분
|
||||
val activeImages = if (activeLimit > 0) {
|
||||
imageService.pageActiveByCharacterOffset(
|
||||
characterId,
|
||||
activeOffset,
|
||||
activeLimit
|
||||
)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val items = buildList {
|
||||
if (startIndex == 0 && pageLength > 0) add(profileItem)
|
||||
activeImages.forEach { img ->
|
||||
val isOwned = (img.imagePriceCan == 0L) || imageService.isOwnedImageByMember(img.id!!, member.id!!)
|
||||
val url = if (isOwned) {
|
||||
imageCloudFront.generateSignedURL(img.imagePath, expiration)
|
||||
} else {
|
||||
"$imageHost/${img.blurImagePath}"
|
||||
}
|
||||
add(
|
||||
CharacterImageListItemResponse(
|
||||
id = img.id!!,
|
||||
imageUrl = url,
|
||||
isOwned = isOwned,
|
||||
imagePriceCan = img.imagePriceCan,
|
||||
sortOrder = img.sortOrder
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ApiResponse.ok(
|
||||
CharacterImageListResponse(
|
||||
totalCount = totalCount,
|
||||
|
|
|
@ -35,6 +35,12 @@ interface CharacterImageQueryRepository {
|
|||
offset: Long,
|
||||
limit: Long
|
||||
): List<CharacterImage>
|
||||
|
||||
fun findActiveImagesByCharacterPaged(
|
||||
characterId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<CharacterImage>
|
||||
}
|
||||
|
||||
class CharacterImageQueryRepositoryImpl(
|
||||
|
@ -73,4 +79,22 @@ class CharacterImageQueryRepositoryImpl(
|
|||
.limit(limit)
|
||||
.fetch()
|
||||
}
|
||||
|
||||
override fun findActiveImagesByCharacterPaged(
|
||||
characterId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<CharacterImage> {
|
||||
val ci = QCharacterImage.characterImage
|
||||
return queryFactory
|
||||
.selectFrom(ci)
|
||||
.where(
|
||||
ci.chatCharacter.id.eq(characterId)
|
||||
.and(ci.isActive.isTrue)
|
||||
)
|
||||
.orderBy(ci.sortOrder.asc(), ci.id.asc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.fetch()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,16 @@ class CharacterImageService(
|
|||
return imageRepository.findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(characterId, pageable)
|
||||
}
|
||||
|
||||
// 오프셋/리밋 조회(활성 이미지)
|
||||
fun pageActiveByCharacterOffset(
|
||||
characterId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<CharacterImage> {
|
||||
if (limit <= 0L) return emptyList()
|
||||
return imageRepository.findActiveImagesByCharacterPaged(characterId, offset, limit)
|
||||
}
|
||||
|
||||
// 구매 이력 + 무료로 계산된 보유 수
|
||||
fun countOwnedActiveByCharacterForMember(characterId: Long, memberId: Long): Long {
|
||||
val freeCount = imageRepository.countByChatCharacterIdAndIsActiveTrueAndImagePriceCan(characterId, 0L)
|
||||
|
|
Loading…
Reference in New Issue