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