feat(character-image): 이미지 단독 구매 API 및 결제 연동 추가
- 구매 요청/응답 DTO 추가 - 미보유 시 캔 차감 및 구매 이력 저장 - 서명 URL(5분) 반환
This commit is contained in:
parent
2ac0a5f896
commit
692e060f6d
|
@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.can.use.UseCanCalculate
|
||||||
import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
|
import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
|
||||||
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
|
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
|
||||||
import kr.co.vividnext.sodalive.can.use.UseCanRepository
|
import kr.co.vividnext.sodalive.can.use.UseCanRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.image.CharacterImage
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.content.AudioContent
|
import kr.co.vividnext.sodalive.content.AudioContent
|
||||||
import kr.co.vividnext.sodalive.content.order.Order
|
import kr.co.vividnext.sodalive.content.order.Order
|
||||||
|
@ -327,4 +328,49 @@ class CanPaymentService(
|
||||||
chargeRepository.save(charge)
|
chargeRepository.save(charge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun spendCanForCharacterImage(
|
||||||
|
memberId: Long,
|
||||||
|
needCan: Int,
|
||||||
|
image: CharacterImage,
|
||||||
|
container: String
|
||||||
|
) {
|
||||||
|
val member = memberRepository.findByIdOrNull(id = memberId)
|
||||||
|
?: throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
|
||||||
|
|
||||||
|
val useRewardCan = spendRewardCan(member, needCan, container)
|
||||||
|
val useChargeCan = if (needCan - useRewardCan.total > 0) {
|
||||||
|
spendChargeCan(member, needCan = needCan - useRewardCan.total, container = container)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needCan - useRewardCan.total - (useChargeCan?.total ?: 0) > 0) {
|
||||||
|
throw SodaException(
|
||||||
|
"${needCan - useRewardCan.total - (useChargeCan?.total ?: 0)} " +
|
||||||
|
"캔이 부족합니다. 충전 후 이용해 주세요."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useRewardCan.verify() || useChargeCan?.verify() == false) {
|
||||||
|
throw SodaException("잘못된 요청입니다.\n다시 시도해 주세요.")
|
||||||
|
}
|
||||||
|
|
||||||
|
val useCan = UseCan(
|
||||||
|
canUsage = CanUsage.CHARACTER_IMAGE_PURCHASE,
|
||||||
|
can = useChargeCan?.total ?: 0,
|
||||||
|
rewardCan = useRewardCan.total,
|
||||||
|
isSecret = false
|
||||||
|
)
|
||||||
|
useCan.member = member
|
||||||
|
useCan.characterImage = image
|
||||||
|
|
||||||
|
useCanRepository.save(useCan)
|
||||||
|
|
||||||
|
setUseCanCalculate(null, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.PG)
|
||||||
|
setUseCanCalculate(null, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.POINT_CLICK_AD)
|
||||||
|
setUseCanCalculate(null, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.GOOGLE_IAP)
|
||||||
|
setUseCanCalculate(null, useRewardCan, useChargeCan, useCan, paymentGateway = PaymentGateway.APPLE_IAP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package kr.co.vividnext.sodalive.chat.character.image
|
package kr.co.vividnext.sodalive.chat.character.image
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.aws.cloudfront.ImageContentCloudFront
|
import kr.co.vividnext.sodalive.aws.cloudfront.ImageContentCloudFront
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
|
||||||
import kr.co.vividnext.sodalive.chat.character.image.dto.CharacterImageListItemResponse
|
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.chat.character.image.dto.CharacterImageListResponse
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.image.dto.CharacterImagePurchaseRequest
|
||||||
|
import kr.co.vividnext.sodalive.chat.character.image.dto.CharacterImagePurchaseResponse
|
||||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
@ -10,6 +13,8 @@ import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.data.domain.PageRequest
|
import org.springframework.data.domain.PageRequest
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
import org.springframework.web.bind.annotation.RequestMapping
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
import org.springframework.web.bind.annotation.RequestParam
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
import org.springframework.web.bind.annotation.RestController
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
@ -19,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController
|
||||||
class CharacterImageController(
|
class CharacterImageController(
|
||||||
private val imageService: CharacterImageService,
|
private val imageService: CharacterImageService,
|
||||||
private val imageCloudFront: ImageContentCloudFront,
|
private val imageCloudFront: ImageContentCloudFront,
|
||||||
|
private val canPaymentService: CanPaymentService,
|
||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val imageHost: String
|
private val imageHost: String
|
||||||
) {
|
) {
|
||||||
|
@ -67,4 +73,35 @@ class CharacterImageController(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/purchase")
|
||||||
|
fun purchase(
|
||||||
|
@RequestBody req: CharacterImagePurchaseRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||||
|
|
||||||
|
val image = imageService.getById(req.imageId)
|
||||||
|
if (!image.isActive) throw SodaException("비활성화된 이미지입니다.")
|
||||||
|
|
||||||
|
val isOwned = (image.imagePriceCan == 0L) ||
|
||||||
|
imageService.isOwnedImageByMember(image.id!!, member.id!!)
|
||||||
|
|
||||||
|
if (!isOwned) {
|
||||||
|
val needCan = image.imagePriceCan.toInt()
|
||||||
|
if (needCan <= 0) throw SodaException("구매 가격이 잘못되었습니다.")
|
||||||
|
|
||||||
|
canPaymentService.spendCanForCharacterImage(
|
||||||
|
memberId = member.id!!,
|
||||||
|
needCan = needCan,
|
||||||
|
image = image,
|
||||||
|
container = req.container
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val expiration = 5L * 60L * 1000L // 5분
|
||||||
|
val signedUrl = imageCloudFront.generateSignedURL(image.imagePath, expiration)
|
||||||
|
ApiResponse.ok(CharacterImagePurchaseResponse(imageUrl = signedUrl))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package kr.co.vividnext.sodalive.chat.character.image.dto
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
data class CharacterImagePurchaseRequest(
|
||||||
|
@JsonProperty("imageId") val imageId: Long,
|
||||||
|
@JsonProperty("container") val container: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CharacterImagePurchaseResponse(
|
||||||
|
@JsonProperty("imageUrl") val imageUrl: String
|
||||||
|
)
|
Loading…
Reference in New Issue