feat(character-image): 이미지 단독 구매 API 및 결제 연동 추가
- 구매 요청/응답 DTO 추가 - 미보유 시 캔 차감 및 구매 이력 저장 - 서명 URL(5분) 반환
This commit is contained in:
		| @@ -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.UseCanCalculateStatus | ||||
| 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.content.AudioContent | ||||
| import kr.co.vividnext.sodalive.content.order.Order | ||||
| @@ -327,4 +328,49 @@ class CanPaymentService( | ||||
|             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 | ||||
|  | ||||
| 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.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.SodaException | ||||
| 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.security.core.annotation.AuthenticationPrincipal | ||||
| 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.RequestParam | ||||
| import org.springframework.web.bind.annotation.RestController | ||||
| @@ -19,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController | ||||
| class CharacterImageController( | ||||
|     private val imageService: CharacterImageService, | ||||
|     private val imageCloudFront: ImageContentCloudFront, | ||||
|     private val canPaymentService: CanPaymentService, | ||||
|     @Value("\${cloud.aws.cloud-front.host}") | ||||
|     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 | ||||
| ) | ||||
		Reference in New Issue
	
	Block a user