feat(chat-character-image): 캐릭터 이미지 리스트 API 추가 및 보유 판단 로직 적용
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package kr.co.vividnext.sodalive.chat.character.image
|
||||
|
||||
import kr.co.vividnext.sodalive.aws.cloudfront.ImageContentCloudFront
|
||||
import kr.co.vividnext.sodalive.chat.character.image.dto.CharacterImageListItemResponse
|
||||
import kr.co.vividnext.sodalive.chat.character.image.dto.CharacterImageListResponse
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/chat/character/image")
|
||||
class CharacterImageController(
|
||||
private val imageService: CharacterImageService,
|
||||
private val imageCloudFront: ImageContentCloudFront,
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val imageHost: String
|
||||
) {
|
||||
|
||||
@GetMapping("/list")
|
||||
fun list(
|
||||
@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 pageable = PageRequest.of(page, pageSize)
|
||||
|
||||
val pageResult = imageService.pageActiveByCharacter(characterId, pageable)
|
||||
val totalCount = pageResult.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,
|
||||
isAdult = img.isAdult,
|
||||
sortOrder = img.sortOrder
|
||||
)
|
||||
}
|
||||
|
||||
ApiResponse.ok(
|
||||
CharacterImageListResponse(
|
||||
totalCount = totalCount,
|
||||
ownedCount = ownedCount,
|
||||
items = items
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.chat.character.image
|
||||
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.stereotype.Repository
|
||||
@@ -8,6 +10,13 @@ import org.springframework.stereotype.Repository
|
||||
interface CharacterImageRepository : JpaRepository<CharacterImage, Long> {
|
||||
fun findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(characterId: Long): List<CharacterImage>
|
||||
|
||||
fun findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(
|
||||
characterId: Long,
|
||||
pageable: Pageable
|
||||
): Page<CharacterImage>
|
||||
|
||||
fun countByChatCharacterIdAndIsActiveTrueAndImagePriceCan(characterId: Long, imagePriceCan: Long): Long
|
||||
|
||||
@Query(
|
||||
"SELECT COALESCE(MAX(ci.sortOrder), 0) FROM CharacterImage ci " +
|
||||
"WHERE ci.chatCharacter.id = :characterId AND ci.isActive = true"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package kr.co.vividnext.sodalive.chat.character.image
|
||||
|
||||
// ktlint-disable standard:max-line-length
|
||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@@ -9,13 +13,46 @@ import org.springframework.transaction.annotation.Transactional
|
||||
class CharacterImageService(
|
||||
private val characterRepository: ChatCharacterRepository,
|
||||
private val imageRepository: CharacterImageRepository,
|
||||
private val triggerTagRepository: CharacterImageTriggerRepository
|
||||
private val triggerTagRepository: CharacterImageTriggerRepository,
|
||||
private val useCanRepository: kr.co.vividnext.sodalive.can.use.UseCanRepository
|
||||
) {
|
||||
|
||||
fun listActiveByCharacter(characterId: Long): List<CharacterImage> {
|
||||
return imageRepository.findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(characterId)
|
||||
}
|
||||
|
||||
// 페이징 조회(활성 이미지)
|
||||
fun pageActiveByCharacter(characterId: Long, pageable: Pageable): Page<CharacterImage> {
|
||||
return imageRepository.findByChatCharacterIdAndIsActiveTrueOrderBySortOrderAsc(characterId, pageable)
|
||||
}
|
||||
|
||||
// 구매 이력 + 무료로 계산된 보유 수
|
||||
fun countOwnedActiveByCharacterForMember(characterId: Long, memberId: Long): Long {
|
||||
val freeCount = imageRepository.countByChatCharacterIdAndIsActiveTrueAndImagePriceCan(characterId, 0L)
|
||||
val purchasedCount = useCanRepository.countPurchasedActiveImagesByCharacter(
|
||||
memberId,
|
||||
characterId,
|
||||
listOf(
|
||||
CanUsage.CHAT_MESSAGE_PURCHASE,
|
||||
CanUsage.CHARACTER_IMAGE_PURCHASE
|
||||
)
|
||||
)
|
||||
return freeCount + purchasedCount
|
||||
}
|
||||
|
||||
fun isOwnedImageByMember(imageId: Long, memberId: Long): Boolean {
|
||||
// 무료이거나(컨트롤러에서 가격 확인) 구매 이력이 있으면 보유로 판단
|
||||
val purchased = useCanRepository.existsByMember_IdAndIsRefundFalseAndCharacterImage_IdAndCanUsageIn(
|
||||
memberId,
|
||||
imageId,
|
||||
listOf(
|
||||
CanUsage.CHAT_MESSAGE_PURCHASE,
|
||||
CanUsage.CHARACTER_IMAGE_PURCHASE
|
||||
)
|
||||
)
|
||||
return purchased
|
||||
}
|
||||
|
||||
fun getById(id: Long): CharacterImage =
|
||||
imageRepository.findById(id).orElseThrow { SodaException("캐릭터 이미지를 찾을 수 없습니다: $id") }
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package kr.co.vividnext.sodalive.chat.character.image.dto
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
data class CharacterImageListItemResponse(
|
||||
@JsonProperty("id") val id: Long,
|
||||
@JsonProperty("imageUrl") val imageUrl: String,
|
||||
@JsonProperty("isOwned") val isOwned: Boolean,
|
||||
@JsonProperty("imagePriceCan") val imagePriceCan: Long,
|
||||
@JsonProperty("isAdult") val isAdult: Boolean,
|
||||
@JsonProperty("sortOrder") val sortOrder: Int
|
||||
)
|
||||
|
||||
data class CharacterImageListResponse(
|
||||
@JsonProperty("totalCount") val totalCount: Long,
|
||||
@JsonProperty("ownedCount") val ownedCount: Long,
|
||||
@JsonProperty("items") val items: List<CharacterImageListItemResponse>
|
||||
)
|
||||
Reference in New Issue
Block a user