캐릭터 챗봇 #338
| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user