| @@ -59,6 +59,9 @@ dependencies { | |||||||
|     // firebase admin sdk |     // firebase admin sdk | ||||||
|     implementation("com.google.firebase:firebase-admin:9.2.0") |     implementation("com.google.firebase:firebase-admin:9.2.0") | ||||||
|  |  | ||||||
|  |     // android publisher | ||||||
|  |     implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20240319-2.0.0") | ||||||
|  |  | ||||||
|     implementation("org.apache.poi:poi-ooxml:5.2.3") |     implementation("org.apache.poi:poi-ooxml:5.2.3") | ||||||
|  |  | ||||||
|     developmentOnly("org.springframework.boot:spring-boot-devtools") |     developmentOnly("org.springframework.boot:spring-boot-devtools") | ||||||
|   | |||||||
| @@ -19,7 +19,13 @@ interface AdminContentRepository : JpaRepository<AudioContent, Long>, AdminAudio | |||||||
|  |  | ||||||
| interface AdminAudioContentQueryRepository { | interface AdminAudioContentQueryRepository { | ||||||
|     fun getAudioContentTotalCount(searchWord: String = ""): Int |     fun getAudioContentTotalCount(searchWord: String = ""): Int | ||||||
|     fun getAudioContentList(offset: Long, limit: Long, searchWord: String = ""): List<GetAdminContentListItem> |     fun getAudioContentList( | ||||||
|  |         imageHost: String, | ||||||
|  |         offset: Long, | ||||||
|  |         limit: Long, | ||||||
|  |         searchWord: String = "" | ||||||
|  |     ): List<GetAdminContentListItem> | ||||||
|  |  | ||||||
|     fun getHashTagList(audioContentId: Long): List<String> |     fun getHashTagList(audioContentId: Long): List<String> | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -46,7 +52,12 @@ class AdminAudioContentQueryRepositoryImpl( | |||||||
|             .size |             .size | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun getAudioContentList(offset: Long, limit: Long, searchWord: String): List<GetAdminContentListItem> { |     override fun getAudioContentList( | ||||||
|  |         imageHost: String, | ||||||
|  |         offset: Long, | ||||||
|  |         limit: Long, | ||||||
|  |         searchWord: String | ||||||
|  |     ): List<GetAdminContentListItem> { | ||||||
|         var where = audioContent.duration.isNotNull |         var where = audioContent.duration.isNotNull | ||||||
|             .and(audioContent.member.isNotNull) |             .and(audioContent.member.isNotNull) | ||||||
|             .and(audioContent.isActive.isTrue.or(audioContent.releaseDate.isNotNull)) |             .and(audioContent.isActive.isTrue.or(audioContent.releaseDate.isNotNull)) | ||||||
| @@ -66,7 +77,7 @@ class AdminAudioContentQueryRepositoryImpl( | |||||||
|                     audioContent.detail, |                     audioContent.detail, | ||||||
|                     audioContentCuration.title, |                     audioContentCuration.title, | ||||||
|                     audioContentCuration.id.nullif(0), |                     audioContentCuration.id.nullif(0), | ||||||
|                     audioContent.coverImage, |                     audioContent.coverImage.prepend("/").prepend(imageHost), | ||||||
|                     audioContent.member!!.nickname, |                     audioContent.member!!.nickname, | ||||||
|                     audioContentTheme.theme, |                     audioContentTheme.theme, | ||||||
|                     audioContentTheme.id, |                     audioContentTheme.id, | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ class AdminContentService( | |||||||
|     fun getAudioContentList(pageable: Pageable): GetAdminContentListResponse { |     fun getAudioContentList(pageable: Pageable): GetAdminContentListResponse { | ||||||
|         val totalCount = repository.getAudioContentTotalCount() |         val totalCount = repository.getAudioContentTotalCount() | ||||||
|         val audioContentAndThemeList = repository.getAudioContentList( |         val audioContentAndThemeList = repository.getAudioContentList( | ||||||
|  |             imageHost = coverImageHost, | ||||||
|             offset = pageable.offset, |             offset = pageable.offset, | ||||||
|             limit = pageable.pageSize.toLong() |             limit = pageable.pageSize.toLong() | ||||||
|         ) |         ) | ||||||
| @@ -43,10 +44,6 @@ class AdminContentService( | |||||||
|                 ) |                 ) | ||||||
|                 it |                 it | ||||||
|             } |             } | ||||||
|             .map { |  | ||||||
|                 it.coverImageUrl = "$coverImageHost/${it.coverImageUrl}" |  | ||||||
|                 it |  | ||||||
|             } |  | ||||||
|             .toList() |             .toList() | ||||||
|  |  | ||||||
|         return GetAdminContentListResponse(totalCount, audioContentList) |         return GetAdminContentListResponse(totalCount, audioContentList) | ||||||
| @@ -56,6 +53,7 @@ class AdminContentService( | |||||||
|         if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") |         if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") | ||||||
|         val totalCount = repository.getAudioContentTotalCount(searchWord) |         val totalCount = repository.getAudioContentTotalCount(searchWord) | ||||||
|         val audioContentAndThemeList = repository.getAudioContentList( |         val audioContentAndThemeList = repository.getAudioContentList( | ||||||
|  |             imageHost = coverImageHost, | ||||||
|             offset = pageable.offset, |             offset = pageable.offset, | ||||||
|             limit = pageable.pageSize.toLong(), |             limit = pageable.pageSize.toLong(), | ||||||
|             searchWord = searchWord |             searchWord = searchWord | ||||||
| @@ -77,10 +75,6 @@ class AdminContentService( | |||||||
|                 ) |                 ) | ||||||
|                 it |                 it | ||||||
|             } |             } | ||||||
|             .map { |  | ||||||
|                 it.coverImageUrl = "$coverImageHost/${it.coverImageUrl}" |  | ||||||
|                 it |  | ||||||
|             } |  | ||||||
|             .toList() |             .toList() | ||||||
|  |  | ||||||
|         return GetAdminContentListResponse(totalCount, audioContentList) |         return GetAdminContentListResponse(totalCount, audioContentList) | ||||||
|   | |||||||
| @@ -49,4 +49,22 @@ class ChargeController(private val service: ChargeService) { | |||||||
|         @RequestBody verifyRequest: AppleVerifyRequest, |         @RequestBody verifyRequest: AppleVerifyRequest, | ||||||
|         @AuthenticationPrincipal user: User |         @AuthenticationPrincipal user: User | ||||||
|     ) = ApiResponse.ok(service.appleVerify(user, verifyRequest)) |     ) = ApiResponse.ok(service.appleVerify(user, verifyRequest)) | ||||||
|  |  | ||||||
|  |     @PostMapping("/google") | ||||||
|  |     fun googleCharge( | ||||||
|  |         @RequestBody request: GoogleChargeRequest, | ||||||
|  |         @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? | ||||||
|  |     ) = run { | ||||||
|  |         if (member == null) { | ||||||
|  |             throw SodaException("로그인 정보를 확인해주세요.") | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ApiResponse.ok(service.googleCharge(member, request)) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @PostMapping("/google/verify") | ||||||
|  |     fun googleVerify( | ||||||
|  |         @RequestBody request: GoogleVerifyRequest, | ||||||
|  |         @AuthenticationPrincipal user: User | ||||||
|  |     ) = ApiResponse.ok(service.googleVerify(user, request)) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -33,3 +33,13 @@ data class AppleChargeRequest( | |||||||
| data class AppleVerifyRequest(val receiptString: String, val chargeId: Long) | data class AppleVerifyRequest(val receiptString: String, val chargeId: Long) | ||||||
|  |  | ||||||
| data class AppleVerifyResponse(val status: Int) | data class AppleVerifyResponse(val status: Int) | ||||||
|  |  | ||||||
|  | data class GoogleChargeRequest( | ||||||
|  |     val title: String, | ||||||
|  |     val chargeCan: Int, | ||||||
|  |     val price: Double, | ||||||
|  |     val currencyCode: String, | ||||||
|  |     val paymentGateway: PaymentGateway | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | data class GoogleVerifyRequest(val productId: String, val purchaseToken: String, val chargeId: Long) | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| package kr.co.vividnext.sodalive.can.charge | package kr.co.vividnext.sodalive.can.charge | ||||||
|  |  | ||||||
| import com.fasterxml.jackson.databind.ObjectMapper | import com.fasterxml.jackson.databind.ObjectMapper | ||||||
|  | import com.google.api.services.androidpublisher.AndroidPublisher | ||||||
| import kr.co.bootpay.Bootpay | import kr.co.bootpay.Bootpay | ||||||
| import kr.co.vividnext.sodalive.can.CanRepository | import kr.co.vividnext.sodalive.can.CanRepository | ||||||
| import kr.co.vividnext.sodalive.can.charge.event.ChargeSpringEvent | import kr.co.vividnext.sodalive.can.charge.event.ChargeSpringEvent | ||||||
| @@ -36,6 +37,8 @@ class ChargeService( | |||||||
|     private val okHttpClient: OkHttpClient, |     private val okHttpClient: OkHttpClient, | ||||||
|     private val applicationEventPublisher: ApplicationEventPublisher, |     private val applicationEventPublisher: ApplicationEventPublisher, | ||||||
|  |  | ||||||
|  |     private val androidPublisher: AndroidPublisher, | ||||||
|  |  | ||||||
|     @Value("\${bootpay.application-id}") |     @Value("\${bootpay.application-id}") | ||||||
|     private val bootpayApplicationId: String, |     private val bootpayApplicationId: String, | ||||||
|     @Value("\${bootpay.private-key}") |     @Value("\${bootpay.private-key}") | ||||||
| @@ -183,6 +186,53 @@ class ChargeService( | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @Transactional | ||||||
|  |     fun googleCharge(member: Member, request: GoogleChargeRequest): ChargeResponse { | ||||||
|  |         val charge = Charge(request.chargeCan, 0) | ||||||
|  |         charge.title = request.title | ||||||
|  |         charge.member = member | ||||||
|  |  | ||||||
|  |         val payment = Payment(paymentGateway = request.paymentGateway) | ||||||
|  |         payment.locale = request.currencyCode | ||||||
|  |         payment.price = request.price | ||||||
|  |  | ||||||
|  |         charge.payment = payment | ||||||
|  |         chargeRepository.save(charge) | ||||||
|  |  | ||||||
|  |         return ChargeResponse(chargeId = charge.id!!) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun googleVerify(user: User, request: GoogleVerifyRequest) { | ||||||
|  |         val charge = chargeRepository.findByIdOrNull(request.chargeId) | ||||||
|  |             ?: throw SodaException("결제정보에 오류가 있습니다.") | ||||||
|  |         val member = memberRepository.findByEmail(user.username) | ||||||
|  |             ?: throw SodaException("로그인 정보를 확인해주세요.") | ||||||
|  |  | ||||||
|  |         if (charge.payment!!.paymentGateway == PaymentGateway.GOOGLE_IAP) { | ||||||
|  |             val response = androidPublisher.purchases().products() | ||||||
|  |                 .get("kr.co.vividnext.sodalive", request.productId, request.purchaseToken) | ||||||
|  |                 .execute() | ||||||
|  |  | ||||||
|  |             if (response.purchaseState == 0) { | ||||||
|  |                 charge.payment?.receiptId = response.purchaseToken | ||||||
|  |                 charge.payment?.method = "구글(인 앱 결제)" | ||||||
|  |                 charge.payment?.status = PaymentStatus.COMPLETE | ||||||
|  |                 member.charge(charge.chargeCan, charge.rewardCan, "aos") | ||||||
|  |  | ||||||
|  |                 applicationEventPublisher.publishEvent( | ||||||
|  |                     ChargeSpringEvent( | ||||||
|  |                         chargeId = charge.id!!, | ||||||
|  |                         memberId = member.id!! | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             } else { | ||||||
|  |                 throw SodaException("결제정보에 오류가 있습니다.") | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             throw SodaException("결제정보에 오류가 있습니다.") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private fun requestRealServerVerify(verifyRequest: AppleVerifyRequest): Boolean { |     private fun requestRealServerVerify(verifyRequest: AppleVerifyRequest): Boolean { | ||||||
|         val body = JSONObject() |         val body = JSONObject() | ||||||
|         body.put("receipt-data", verifyRequest.receiptString) |         body.put("receipt-data", verifyRequest.receiptString) | ||||||
|   | |||||||
| @@ -0,0 +1,31 @@ | |||||||
|  | package kr.co.vividnext.sodalive.configs | ||||||
|  |  | ||||||
|  | import com.google.api.client.http.javanet.NetHttpTransport | ||||||
|  | import com.google.api.client.json.gson.GsonFactory | ||||||
|  | import com.google.api.services.androidpublisher.AndroidPublisher | ||||||
|  | import com.google.api.services.androidpublisher.AndroidPublisherScopes | ||||||
|  | import com.google.auth.http.HttpCredentialsAdapter | ||||||
|  | import com.google.auth.oauth2.GoogleCredentials | ||||||
|  | import org.springframework.beans.factory.annotation.Value | ||||||
|  | import org.springframework.context.annotation.Bean | ||||||
|  | import org.springframework.context.annotation.Configuration | ||||||
|  | import java.io.FileInputStream | ||||||
|  |  | ||||||
|  | @Configuration | ||||||
|  | class AndroidPublisherConfig( | ||||||
|  |     @Value("\${firebase.secret-key-path}") | ||||||
|  |     private val secretKeyPath: String | ||||||
|  | ) { | ||||||
|  |     @Bean | ||||||
|  |     fun androidPublisher(): AndroidPublisher { | ||||||
|  |         val jsonFactory = GsonFactory.getDefaultInstance() | ||||||
|  |         val httpTransport = NetHttpTransport() | ||||||
|  |  | ||||||
|  |         val credential = GoogleCredentials.fromStream(FileInputStream(secretKeyPath)) | ||||||
|  |             .createScoped(listOf(AndroidPublisherScopes.ANDROIDPUBLISHER)) | ||||||
|  |  | ||||||
|  |         return AndroidPublisher.Builder(httpTransport, jsonFactory, HttpCredentialsAdapter(credential)) | ||||||
|  |             .setApplicationName("소다라이브") | ||||||
|  |             .build() | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user