diff --git a/build.gradle.kts b/build.gradle.kts index e34510a..078e33d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -59,6 +59,9 @@ dependencies { // firebase admin sdk 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") developmentOnly("org.springframework.boot:spring-boot-devtools") diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt index 1a0288e..4164c93 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt @@ -19,7 +19,13 @@ interface AdminContentRepository : JpaRepository, AdminAudio interface AdminAudioContentQueryRepository { fun getAudioContentTotalCount(searchWord: String = ""): Int - fun getAudioContentList(offset: Long, limit: Long, searchWord: String = ""): List + fun getAudioContentList( + imageHost: String, + offset: Long, + limit: Long, + searchWord: String = "" + ): List + fun getHashTagList(audioContentId: Long): List } @@ -46,7 +52,12 @@ class AdminAudioContentQueryRepositoryImpl( .size } - override fun getAudioContentList(offset: Long, limit: Long, searchWord: String): List { + override fun getAudioContentList( + imageHost: String, + offset: Long, + limit: Long, + searchWord: String + ): List { var where = audioContent.duration.isNotNull .and(audioContent.member.isNotNull) .and(audioContent.isActive.isTrue.or(audioContent.releaseDate.isNotNull)) @@ -66,7 +77,7 @@ class AdminAudioContentQueryRepositoryImpl( audioContent.detail, audioContentCuration.title, audioContentCuration.id.nullif(0), - audioContent.coverImage, + audioContent.coverImage.prepend("/").prepend(imageHost), audioContent.member!!.nickname, audioContentTheme.theme, audioContentTheme.id, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt index a421759..5b3e8a7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt @@ -23,6 +23,7 @@ class AdminContentService( fun getAudioContentList(pageable: Pageable): GetAdminContentListResponse { val totalCount = repository.getAudioContentTotalCount() val audioContentAndThemeList = repository.getAudioContentList( + imageHost = coverImageHost, offset = pageable.offset, limit = pageable.pageSize.toLong() ) @@ -43,10 +44,6 @@ class AdminContentService( ) it } - .map { - it.coverImageUrl = "$coverImageHost/${it.coverImageUrl}" - it - } .toList() return GetAdminContentListResponse(totalCount, audioContentList) @@ -56,6 +53,7 @@ class AdminContentService( if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.") val totalCount = repository.getAudioContentTotalCount(searchWord) val audioContentAndThemeList = repository.getAudioContentList( + imageHost = coverImageHost, offset = pageable.offset, limit = pageable.pageSize.toLong(), searchWord = searchWord @@ -77,10 +75,6 @@ class AdminContentService( ) it } - .map { - it.coverImageUrl = "$coverImageHost/${it.coverImageUrl}" - it - } .toList() return GetAdminContentListResponse(totalCount, audioContentList) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt index 6f6291b..27b6cea 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeController.kt @@ -49,4 +49,22 @@ class ChargeController(private val service: ChargeService) { @RequestBody verifyRequest: AppleVerifyRequest, @AuthenticationPrincipal user: User ) = 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)) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeData.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeData.kt index 7e7374a..a494333 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeData.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeData.kt @@ -33,3 +33,13 @@ data class AppleChargeRequest( data class AppleVerifyRequest(val receiptString: String, val chargeId: Long) 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) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt index d711cb5..294ee97 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.can.charge import com.fasterxml.jackson.databind.ObjectMapper +import com.google.api.services.androidpublisher.AndroidPublisher import kr.co.bootpay.Bootpay import kr.co.vividnext.sodalive.can.CanRepository import kr.co.vividnext.sodalive.can.charge.event.ChargeSpringEvent @@ -36,6 +37,8 @@ class ChargeService( private val okHttpClient: OkHttpClient, private val applicationEventPublisher: ApplicationEventPublisher, + private val androidPublisher: AndroidPublisher, + @Value("\${bootpay.application-id}") private val bootpayApplicationId: String, @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 { val body = JSONObject() body.put("receipt-data", verifyRequest.receiptString) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/AndroidPublisherConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/AndroidPublisherConfig.kt new file mode 100644 index 0000000..8dada68 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/AndroidPublisherConfig.kt @@ -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() + } +}