From 0347d767f0e2db9a6bb20e883353efc0729fc3b7 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 27 Aug 2025 14:22:07 +0900 Subject: [PATCH] =?UTF-8?q?feat(character-image):=20=EC=BA=90=EB=A6=AD?= =?UTF-8?q?=ED=84=B0=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B2=AB=20=EC=B9=B8=EC=97=90=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=8F=AC=ED=95=A8=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8E=98=EC=9D=B4=EC=A7=95=20=EB=B3=B4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 경험 향상을 위해 캐릭터 프로필 이미지를 이미지 리스트의 맨 앞에 노출하도록 변경. --- .../image/CharacterImageController.kt | 79 ++++++++++++++----- .../image/CharacterImageRepository.kt | 24 ++++++ .../character/image/CharacterImageService.kt | 10 +++ 3 files changed, 95 insertions(+), 18 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageController.kt index c131b73..057ca9c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageController.kt @@ -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, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageRepository.kt index c8a4e0e..f23c7e8 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageRepository.kt @@ -35,6 +35,12 @@ interface CharacterImageQueryRepository { offset: Long, limit: Long ): List + + fun findActiveImagesByCharacterPaged( + characterId: Long, + offset: Long, + limit: Long + ): List } class CharacterImageQueryRepositoryImpl( @@ -73,4 +79,22 @@ class CharacterImageQueryRepositoryImpl( .limit(limit) .fetch() } + + override fun findActiveImagesByCharacterPaged( + characterId: Long, + offset: Long, + limit: Long + ): List { + 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() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageService.kt index 751b814..b0bbe98 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageService.kt @@ -26,6 +26,16 @@ class CharacterImageService( return imageRepository.findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(characterId, pageable) } + // 오프셋/리밋 조회(활성 이미지) + fun pageActiveByCharacterOffset( + characterId: Long, + offset: Long, + limit: Long + ): List { + if (limit <= 0L) return emptyList() + return imageRepository.findActiveImagesByCharacterPaged(characterId, offset, limit) + } + // 구매 이력 + 무료로 계산된 보유 수 fun countOwnedActiveByCharacterForMember(characterId: Long, memberId: Long): Long { val freeCount = imageRepository.countByChatCharacterIdAndIsActiveTrueAndImagePriceCan(characterId, 0L)