캐릭터 챗봇 #338

Merged
klaus merged 119 commits from test into main 2025-09-10 06:08:47 +00:00
3 changed files with 95 additions and 18 deletions
Showing only changes of commit 0347d767f0 - Show all commits

View File

@ -40,30 +40,73 @@ class CharacterImageController(
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.") if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
val pageSize = if (size <= 0) 20 else minOf(size, 20) 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 ownedCount = imageService.countOwnedActiveByCharacterForMember(characterId, member.id!!)
val expiration = 5L * 60L * 1000L // 5분 val totalCount = totalActiveElements + 1 // 프로필 포함
val items = pageResult.content.map { img ->
val isOwned = (img.imagePriceCan == 0L) || imageService.isOwnedImageByMember(img.id!!, member.id!!) val startIndex = page * pageSize
val url = if (isOwned) { if (startIndex >= totalCount) {
imageCloudFront.generateSignedURL(img.imagePath, expiration) return@run ApiResponse.ok(
} else { CharacterImageListResponse(
"$imageHost/${img.blurImagePath}" totalCount = totalCount,
} ownedCount = ownedCount,
CharacterImageListItemResponse( items = emptyList()
id = img.id!!, )
imageUrl = url,
isOwned = isOwned,
imagePriceCan = img.imagePriceCan,
sortOrder = img.sortOrder
) )
} }
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( ApiResponse.ok(
CharacterImageListResponse( CharacterImageListResponse(
totalCount = totalCount, totalCount = totalCount,

View File

@ -35,6 +35,12 @@ interface CharacterImageQueryRepository {
offset: Long, offset: Long,
limit: Long limit: Long
): List<CharacterImage> ): List<CharacterImage>
fun findActiveImagesByCharacterPaged(
characterId: Long,
offset: Long,
limit: Long
): List<CharacterImage>
} }
class CharacterImageQueryRepositoryImpl( class CharacterImageQueryRepositoryImpl(
@ -73,4 +79,22 @@ class CharacterImageQueryRepositoryImpl(
.limit(limit) .limit(limit)
.fetch() .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()
}
} }

View File

@ -26,6 +26,16 @@ class CharacterImageService(
return imageRepository.findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(characterId, pageable) 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 { fun countOwnedActiveByCharacterForMember(characterId: Long, memberId: Long): Long {
val freeCount = imageRepository.countByChatCharacterIdAndIsActiveTrueAndImagePriceCan(characterId, 0L) val freeCount = imageRepository.countByChatCharacterIdAndIsActiveTrueAndImagePriceCan(characterId, 0L)