캐릭터 챗봇 #338
|
@ -45,7 +45,6 @@ class CharacterImageController(
|
|||
val pageResult = imageService.pageActiveByCharacter(characterId, pageable)
|
||||
val totalCount = pageResult.totalElements
|
||||
|
||||
// 현재 요구사항 기준 '무료=보유'로 계산 (구매 이력은 추후 확장)
|
||||
val ownedCount = imageService.countOwnedActiveByCharacterForMember(characterId, member.id!!)
|
||||
|
||||
val expiration = 5L * 60L * 1000L // 5분
|
||||
|
@ -74,6 +73,82 @@ class CharacterImageController(
|
|||
)
|
||||
}
|
||||
|
||||
@GetMapping("/my-list")
|
||||
fun myList(
|
||||
@RequestParam characterId: Long,
|
||||
@RequestParam(required = false, defaultValue = "0") page: Int,
|
||||
@RequestParam(required = false, defaultValue = "20") size: Int,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||
|
||||
val pageSize = if (size <= 0) 20 else minOf(size, 20)
|
||||
val expiration = 5L * 60L * 1000L // 5분
|
||||
|
||||
val ownedCount = imageService.countOwnedActiveByCharacterForMember(characterId, member.id!!)
|
||||
val totalCount = ownedCount + 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
|
||||
)
|
||||
|
||||
// 보유 이미지의 오프셋/리밋 계산 (결합 리스트 [프로필] + ownedImages)
|
||||
val ownedOffset = if (startIndex == 0) 0L else (startIndex - 1).toLong()
|
||||
val ownedLimit = if (startIndex == 0) (pageLength - 1).toLong() else pageLength.toLong()
|
||||
|
||||
val ownedImagesPage = if (ownedLimit > 0) {
|
||||
imageService.pageOwnedActiveByCharacterForMember(characterId, member.id!!, ownedOffset, ownedLimit)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val items = buildList {
|
||||
if (startIndex == 0 && pageLength > 0) add(profileItem)
|
||||
ownedImagesPage.forEach { img ->
|
||||
val url = imageCloudFront.generateSignedURL(img.imagePath, expiration)
|
||||
add(
|
||||
CharacterImageListItemResponse(
|
||||
id = img.id!!,
|
||||
imageUrl = url,
|
||||
isOwned = true,
|
||||
imagePriceCan = img.imagePriceCan,
|
||||
sortOrder = img.sortOrder
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ApiResponse.ok(
|
||||
CharacterImageListResponse(
|
||||
totalCount = totalCount,
|
||||
ownedCount = ownedCount,
|
||||
items = items
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@PostMapping("/purchase")
|
||||
fun purchase(
|
||||
@RequestBody req: CharacterImagePurchaseRequest,
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package kr.co.vividnext.sodalive.chat.character.image
|
||||
|
||||
import com.querydsl.jpa.JPAExpressions
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
@ -7,7 +11,7 @@ import org.springframework.data.jpa.repository.Query
|
|||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface CharacterImageRepository : JpaRepository<CharacterImage, Long> {
|
||||
interface CharacterImageRepository : JpaRepository<CharacterImage, Long>, CharacterImageQueryRepository {
|
||||
fun findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(characterId: Long): List<CharacterImage>
|
||||
|
||||
fun findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(
|
||||
|
@ -23,3 +27,50 @@ interface CharacterImageRepository : JpaRepository<CharacterImage, Long> {
|
|||
)
|
||||
fun findMaxSortOrderByCharacterId(characterId: Long): Int
|
||||
}
|
||||
|
||||
interface CharacterImageQueryRepository {
|
||||
fun findOwnedActiveImagesByCharacterPaged(
|
||||
characterId: Long,
|
||||
memberId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<CharacterImage>
|
||||
}
|
||||
|
||||
class CharacterImageQueryRepositoryImpl(
|
||||
private val queryFactory: JPAQueryFactory
|
||||
) : CharacterImageQueryRepository {
|
||||
override fun findOwnedActiveImagesByCharacterPaged(
|
||||
characterId: Long,
|
||||
memberId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<CharacterImage> {
|
||||
val usages = listOf(CanUsage.CHAT_MESSAGE_PURCHASE, CanUsage.CHARACTER_IMAGE_PURCHASE)
|
||||
val ci = QCharacterImage.characterImage
|
||||
return queryFactory
|
||||
.selectFrom(ci)
|
||||
.where(
|
||||
ci.chatCharacter.id.eq(characterId)
|
||||
.and(ci.isActive.isTrue)
|
||||
.and(
|
||||
ci.imagePriceCan.eq(0L).or(
|
||||
JPAExpressions
|
||||
.selectOne()
|
||||
.from(useCan)
|
||||
.where(
|
||||
useCan.member.id.eq(memberId)
|
||||
.and(useCan.isRefund.isFalse)
|
||||
.and(useCan.characterImage.id.eq(ci.id))
|
||||
.and(useCan.canUsage.`in`(usages))
|
||||
)
|
||||
.exists()
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderBy(ci.sortOrder.asc(), ci.id.asc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.fetch()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,23 @@ class CharacterImageService(
|
|||
fun getById(id: Long): CharacterImage =
|
||||
imageRepository.findById(id).orElseThrow { SodaException("캐릭터 이미지를 찾을 수 없습니다: $id") }
|
||||
|
||||
fun getCharacterImagePath(characterId: Long): String? {
|
||||
val character = characterRepository.findById(characterId)
|
||||
.orElseThrow { SodaException("캐릭터를 찾을 수 없습니다: $characterId") }
|
||||
return character.imagePath
|
||||
}
|
||||
|
||||
// 보유한(무료+구매) 활성 이미지 페이징 조회
|
||||
fun pageOwnedActiveByCharacterForMember(
|
||||
characterId: Long,
|
||||
memberId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<CharacterImage> {
|
||||
if (limit <= 0L) return emptyList()
|
||||
return imageRepository.findOwnedActiveImagesByCharacterPaged(characterId, memberId, offset, limit)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun registerImage(
|
||||
characterId: Long,
|
||||
|
|
Loading…
Reference in New Issue