캐릭터 챗봇 #338
|
@ -45,7 +45,6 @@ class CharacterImageController(
|
||||||
val pageResult = imageService.pageActiveByCharacter(characterId, pageable)
|
val pageResult = imageService.pageActiveByCharacter(characterId, pageable)
|
||||||
val totalCount = pageResult.totalElements
|
val totalCount = pageResult.totalElements
|
||||||
|
|
||||||
// 현재 요구사항 기준 '무료=보유'로 계산 (구매 이력은 추후 확장)
|
|
||||||
val ownedCount = imageService.countOwnedActiveByCharacterForMember(characterId, member.id!!)
|
val ownedCount = imageService.countOwnedActiveByCharacterForMember(characterId, member.id!!)
|
||||||
|
|
||||||
val expiration = 5L * 60L * 1000L // 5분
|
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")
|
@PostMapping("/purchase")
|
||||||
fun purchase(
|
fun purchase(
|
||||||
@RequestBody req: CharacterImagePurchaseRequest,
|
@RequestBody req: CharacterImagePurchaseRequest,
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
package kr.co.vividnext.sodalive.chat.character.image
|
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.Page
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
@ -7,7 +11,7 @@ import org.springframework.data.jpa.repository.Query
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
interface CharacterImageRepository : JpaRepository<CharacterImage, Long> {
|
interface CharacterImageRepository : JpaRepository<CharacterImage, Long>, CharacterImageQueryRepository {
|
||||||
fun findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(characterId: Long): List<CharacterImage>
|
fun findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(characterId: Long): List<CharacterImage>
|
||||||
|
|
||||||
fun findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(
|
fun findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(
|
||||||
|
@ -23,3 +27,50 @@ interface CharacterImageRepository : JpaRepository<CharacterImage, Long> {
|
||||||
)
|
)
|
||||||
fun findMaxSortOrderByCharacterId(characterId: Long): Int
|
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 =
|
fun getById(id: Long): CharacterImage =
|
||||||
imageRepository.findById(id).orElseThrow { SodaException("캐릭터 이미지를 찾을 수 없습니다: $id") }
|
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
|
@Transactional
|
||||||
fun registerImage(
|
fun registerImage(
|
||||||
characterId: Long,
|
characterId: Long,
|
||||||
|
|
Loading…
Reference in New Issue