#billcorea #운동동아리관리앱 🏸 Schneedle — незаменимое приложение для бадминтонных клубов! 👉 Матчевая игра: записывайте результаты и находите противников 🎉 Идеально подходит для любого места: в одиночку, с друзьями или в клубе! 🤝 Если вам нравится бадминтон, обязательно попробуйте
Зайди в приложение 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay
다음 해당 프로젝트에서 APIs을 사용할 수 있도록 설정하는 단계입니다. 프로젝트를 확인하고 APIs을 사용하도록 설정합니다.
프로젝트 확인
사용설정
3. gCloud Client을 설치하고 설정을 초기화
설정이 완료되었으니, 이제 cloud sdk installer을 다운로드하고 나서 설치를 진행합니다. 설치하는 과정은 next 버튼을 클릭하는 것으로 완료가 됩니다. 시간은 조금 소요됩니다.
GoogleCloudSDKInstaller.exe 를 다운로드 하고 설치 합니다.
설치가 되고 나면
cloud을 위한 powershell을 찾아서 실행합니다. (Windows 11 기준에서)
4. 환경을 초기화합니다.
이제 환경 설정을 해 보겠습니다. gclound init 실행하면 다음과 같이 환경 설정이 시작됩니다. 여기서 누락된 부분은 서버의 스토리지 위치인데, 가급적이면 asia로 해 주는 것이 나중에 실행 시에 도움이 됩니다.
PS C:\workspaces\cloudhome\boss0426> gcloud init Welcome! This command will take you through the configuration of gcloud.
Settings from your current configuration [default] are: accessibility: screen_reader: 'False' core: account: 6****@gmail.com disable_usage_reporting: 'False' project: bespeak-f3bff
Pick configuration to use: [1] Re-initialize this configuration [default] with new settings [2] Create a new configuration Please enter your numeric choice: 2
Enter configuration name. Names start with a lower case letter and contain only lower case letters a-z, digits 0-9, and hyphens '-': bo****ew Your current configuration has been set to: [boss0426-new]
You can skip diagnostics next time by using the following flag: gcloud init --skip-diagnostics
Choose the account you would like to use to perform operations for this configuration: [1] 6***@gmail.com [2] Log in with a new account Please enter your numeric choice: 1
You are logged in as: [6k2emg@gmail.com].
Pick cloud project to use: [1] boss0426-f0490 [2] Enter a project ID [3] Create a new project Please enter numeric choice or text value (must exactly match list item): 1
Not setting default zone/region (this feature makes it easier to use [gcloud compute] by setting an appropriate default value for the --zone and --region flag). See https://cloud.google.com/compute/docs/gcloud-compute section on how to set default compute region and zone manually. If you would like [gcloud init] to be able to do this for you the next time you run it, make sure the Compute Engine API is enabled for your project on the https://console.developers.google.com/apis page.
Your Google Cloud SDK is configured and ready to use!
* Commands that require authentication will use 6k***@gmail.com by default * Commands will reference project `boss0426-f0490` by default Run `gcloud help config` to learn how to change individual settings
This gcloud configuration is called [boss0426-new]. You can create additional configurations if you work with multiple accounts and/or projects. Run `gcloud topic configurations` to learn more.
Some things to try next:
* Run `gcloud --help` to see the Cloud Platform services you can interact with. And run `gcloud help COMMAND` to get help on any gcloud command. * Run `gcloud topic --help` to learn about advanced features of the SDK like arg files and output formatting * Run `gcloud cheat-sheet` to see a roster of go-to `gcloud` commands.
[notice] A new release of pip available: 22.3 -> 22.3.1 [notice] To update, run: python.exe -m pip install --upgrade pip
클라우드 함수 만들기
이제 다시 처음 가이드로 돌아와서 cloud에서 사용할 함수를 만들어 보겠습니다. 저는 python으로 구동하는 함수를 만들기 위해서 환경 설정도 했고 해서 python 으로 돌아가는 함수를 만들 예정입니다.
1. 먼저 python project 폴더를 하나 생성 합니다. (이글에서는 boss0426으로 할 예정입니다.)
2. main.py 코드를 만들고 그 안에 필요한 함수를 구성합니다. 해당 함수의 이름은 deploy 할 때 함수 이름으로 사용되므로 작성 시에 참고하세요. https 호출 시 끝단 URL 이 됩니다.
3. requirements.txt 파일을 하나 작성 해서 import 되어야 하는 항목을 나열해 줍니다. pc에서 python 환경으로 구동할 때는 그냥 source code에 import 하면 실행이 되기는 하지만, cloud 환경에서는 그것이 안 되는 것으로 보입니다. 그래서 text 파일에 모두 기록을 해 주면 실행 시에 load 되어 같이 실행이 되는 것으로 보입니다.
메뉴에서 Code 제일 아래에 보면 Convert Java File to Kotlin File 이 보입니다. 물로 이 메뉴는 Java 코드일 때만 보입니다.
android studio 메뉴
변환을 시행해 보겠습니다. 변환은 내 앱의 상위 package 이름이 나와 있는 위치에서 오른쪽 마우스를 클릭해서 하는 방법도 있습니다. 개발 java 파일을 선택해서 오른쪽 마우스 클릭해서 하게 되는 경우는 개별 파일만 처리해 주지만, 최상위 package을 선택하고 하는 경우 하위 경로에 있는 모든 파일을 한 번에 변환해 줍니다.
주의 사항
일괄 변환된 후에 해야할 일들이 생깁니다. java 코드에서는 global 변수로 사용하고자 하는 경우 그냥 변수 이름만 선언해 주면 되었던 부분들이 kotlin을 변환하게 되면 그 값을 정해 주는 것에 대해서 설정을 해 주어야 하는 부분들이 생기며 해당 변수를 일괄적으로 null 대입하는 코드로 변환을 해 주시기 때문에 아래 예시들처럼 수정을 해 주어야 하는 부분들이 생깁니다.
변환 전 / 후
위 예시는 kotlin 으로 변환 후에 코드를 정리한 후의 코드이니 변환된 직후의 코드와는 다르다는 것을 염두에 두고 보시길 바랍니다.
조금더 자세한 주의 사항을 보시려면 google 에서 제공하는 codelab 을 살펴 보세요.
source code 는 변환을 해 주지만, gradle 파일을 자동 변환을 해 주지 않기 때문에 설정을 일부 추가해 주어야 합니다.
먼저 project 의 gradle 파일에는 아래처럼 2곳에 추가를 해 주었습니다.
buildscript { ext.kotlin_version = '1.7.20' // kotlin 추가 repositories { google() mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // kotlin 추가 // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } }
module의 gradle 파일은 다음과 같이 추가해 주었습니다.
plugins { id 'com.android.application' id 'kotlin-android-extensions' // kotlin 추가 id 'kotlin-android' // kotlin 추가 id 'kotlin-kapt' // kotlin 추가 }
class BillingManager(var mActivity: Activity) : PurchasesUpdatedListener, ConsumeResponseListener { var TAG = "BillingManager" lateinit var mBillingClient: BillingClient lateinit var mSkuDetails: List<SkuDetails>
enum class connectStatusTypes { waiting, connected, fail, disconnected }
var connectStatus = connectStatusTypes.waiting
/** * 구글에 설정한 구독 상품 아이디와 일치 하지 않으면 오류를 발생 시킴. * 21.04.20 이번에는 1회성 구매로 변경 210414_monthly_bill_999, 210420_monthly_bill */ var punchName = "220302_bill_1month_999" var punchNameInapp = "210420_monthly_bill" var payType = BillingClient.SkuType.SUBS var option: SharedPreferences var editor: SharedPreferences.Editor
/** * 정기 결재 소모 여부를 수신 : 21.04.20 1회성 구매의 경우는 결재하면 끝임. * @param billingResult * @param purchaseToken */ override fun onConsumeResponse(billingResult: BillingResult, purchaseToken: String) { if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) { Log.i(TAG, "사용끝 + $purchaseToken") return } else { Log.i(TAG, "소모에 실패 " + billingResult.responseCode + " 대상 상품 " + purchaseToken) return } }
fun purchase(skuDetails: SkuDetails?): Int { val flowParams = BillingFlowParams.newBuilder() .setSkuDetails(skuDetails!!) .build() return mBillingClient.launchBillingFlow(mActivity, flowParams).responseCode }
fun purchaseAsync() { Log.e(TAG, "--------------------------------------------------------------") mBillingClient.queryPurchasesAsync(payType) { billingResult, list -> Log.e(TAG, "onQueryPurchasesResponse=" + billingResult.responseCode) val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") if (list.size < 1) { editor = option.edit() editor.putBoolean("isBill", false) editor.commit() Log.e(TAG, "getData=" + list.size) } else { for (purchase in list) { Log.e(TAG, "getPurchaseToken=" + purchase.purchaseToken) for (str in purchase.skus) { Log.e(TAG, "getSkus=$str") } val now = Date() now.time = purchase.purchaseTime Log.e(TAG, "getPurchaseTime=" + sdf.format(now)) Log.e(TAG, "getQuantity=" + purchase.quantity) Log.e(TAG, "getSignature=" + purchase.signature) Log.e(TAG, "isAutoRenewing=" + purchase.isAutoRenewing) Log.e(TAG, "getPurchaseState=" + purchase.purchaseState) editor = option.edit() editor.putBoolean("isBill", purchase.isAutoRenewing) editor.commit() } } Log.e(TAG, "--------------------------------------------------------------") } }
val skuDetailList: Unit get() { val skuIdList: MutableList<String> = ArrayList() skuIdList.add(punchName) val params = SkuDetailsParams.newBuilder() params.setSkusList(skuIdList).setType(payType) mBillingClient.querySkuDetailsAsync( params.build(), SkuDetailsResponseListener { billingResult, skuDetailsList -> if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) { Log.i(TAG, "detail respCode=" + billingResult.responseCode) return@SkuDetailsResponseListener } if (skuDetailsList == null) { KakaoToast.makeToast( mActivity, mActivity.getString(R.string.msgNotInfo), Toast.LENGTH_LONG ).show() return@SkuDetailsResponseListener } Log.i(TAG, "listCount=" + skuDetailsList.size) for (skuDetails in skuDetailsList) { Log.i(TAG, """ ${skuDetails.sku} ${skuDetails.title} ${skuDetails.price} ${skuDetails.description} ${skuDetails.freeTrialPeriod} ${skuDetails.iconUrl} ${skuDetails.introductoryPrice} ${skuDetails.introductoryPriceAmountMicros} ${skuDetails.originalPrice} ${skuDetails.priceCurrencyCode} """.trimIndent() ) } purchase(skuDetailsList[0]) }) }
/** * @param billingResult * @param purchases */ override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List<Purchase>?) { if (billingResult == null) { Log.wtf(TAG, "onPurchasesUpdated: null BillingResult") return } val responseCode = billingResult.responseCode val debugMessage = billingResult.debugMessage Log.d(TAG, "onPurchasesUpdated: ${responseCode} ${debugMessage}") when (responseCode) { BillingClient.BillingResponseCode.OK -> if (purchases == null) { Log.d(TAG, "onPurchasesUpdated: null purchase list") processPurchases(null) } else { processPurchases(purchases) } BillingClient.BillingResponseCode.USER_CANCELED -> Log.i( TAG, "onPurchasesUpdated: User canceled the purchase" ) BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> Log.i( TAG, "onPurchasesUpdated: The user already owns this item" ) BillingClient.BillingResponseCode.DEVELOPER_ERROR -> Log.e( TAG, "onPurchasesUpdated: Developer error means that Google Play " + "does not recognize the configuration. If you are just getting started, " + "make sure you have configured the application correctly in the " + "Google Play Console. The SKU product ID must match and the APK you " + "are using must be signed with release keys." ) } }
private fun processPurchases(purchasesList: List<Purchase>?) { if (purchasesList != null) { Log.d(TAG, "processPurchases: " + purchasesList.size + " purchase(s)") } else { Log.d(TAG, "processPurchases: with no purchases") } if (isUnchangedPurchaseList(purchasesList)) { Log.d(TAG, "processPurchases: Purchase list has not changed") return } }
/** * subs 의 경우는 아래와 같이 구매확인을 해 주어야 됨. * @param purchase */ fun confirmPerchase(purchase: Purchase) { //PURCHASED if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) { if (!purchase.isAcknowledged) { val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder() .setPurchaseToken(purchase.purchaseToken) .build() mBillingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult -> Log.e(TAG, "getResponseCode=" + billingResult.responseCode) editor.putBoolean("isBill", true) editor.commit() } } } else if (purchase.purchaseState == Purchase.PurchaseState.PENDING) { //구매 유예 Log.e(TAG, "//구매 유예") } else { //구매확정 취소됨(기타 다양한 사유...) Log.e(TAG, "//구매확정 취소됨(기타 다양한 사유...)") } }
private fun isUnchangedPurchaseList(purchasesList: List<Purchase>?): Boolean { for (purchase in purchasesList!!) { confirmPerchase(purchase) } return false } }
이전에 포스팅했던 java 코드와 비교를 해 보면 코드가 많이 간소화되었다는 것을 알 수 있습니다. 호출해서 사용하는 코드는 github의 코드를 참고해 보세요.
결제 테스트
결제 진행에 대한 테스트는 꼭 playstore에 게시한 이후에 진행하여야 합니다. 저는 내부 테스트로 게시한 이후 진행 하고 있습니다. 그리고 정기결제 항목을 처음 등록한 경우에는 해당 결제 항목이 사용이 될 수 있으려면 24시간 이상 걸리는 경우가 있으므로 미리 정기결제 항목을 등록해 두고 앱을 만들어 가는 것이 시간 활용에 도움이 됩니다.
앱에 결제 기능을 다는 이야기는 이전 포스팅에 있습니다. 이번에는 혜택을 주는 방법에 대한 이야기를 적어 봅니다.
먼저 이전에 등록해서 운영하던 경우를 기준으로 작성하고 있음을 밝혀 둡니다. 이전에 만들었던 앱에 매월 정기 결제를 통해 광고를 제거하는 옵션을 달았던 적이 있습니다.
정기 결제(구독)이 등록된 정보
그중에서 현재 운영 중인 구독 보기를 선택합니다.
혜택 추가 하기
혜택 추가
이제 혜택 추가를 해 보겠습니다.
혜택 추가
혜택 추가 하기에는 신규 고객을 선택하는 경우와 이전 사용자를 선택 하는 경우, 그 외 개발자의 임의 지정을 선택할 수 있을 듯합니다. 기존 고객을 위한 프로모션을 하는 경우도 있겠지만, 제가 배포한 앱은 아직 사용자가 없기 때문에 신규 고객을 대상으로 한 혜택 추가를 해 보겠습니다.
자격기준
저 선택 사항 아래 탱크를 달도록 되어 있는 데, 일단은 무시해 보겠습니다.
저장해 보기
그냥 저장 버튼을 눌렀더니 아래와 같이 단계를 추가하도록 가이드를 하고 있습니다. 단계는 2개까지 등록이 될 것 같습니다.
단계 추가
신규 고객에게 혜택을 등록하는 것으로 정했으니, 단계 추가에서는 무료 체험판이라고 선택을 하는 것이 맞을 듯합니다.
무료체험판 : 지정하는 기간 동안 무료 체험을 제공합니다.
1회 결제 : 지정하는 기간 동안 1회 결제에 한하여 정액, 할인율, 일정금액 등으로 가격을 조정해 줄 수 있습니다.
할인된 반복 결제 : 지정하는 결제 기간 동안 정액, 할인율, 일정금액 등으로 가격을 조정해 줄 수 있습니다.
단계 옵션
저는 신규 고객에게 3개월 동안 무료 체험을 할 수 있도록 하고자 합니다. 그랬더니 판매가 되는 국가별로 가격표가 노출이 됩니다. 그리고 적용을 눌러보겠습니다.
앱을 개발 하다 보면 카메라 촬영을 통해서 이미지를 사용하는 앱들도 구현하게 됩니다. 이런 경우 AVD에서 직접 촬영한 이미지를 볼 수 있도록 하면 좋을 것 같습니다. (이미 알고 계시는 경우도 있기는 하겠지만...)
설정해 보기
Android Studio 을 실행하고 AVD을 하나 실행해 보겠습니다. Android Studio Dolphin | 2021.3.1 Patch 1을 기준으로 설명해 드립니다. 이전 버전에서도 지원이 되기는 하니 참고하시면 될 것 같습니다.
먼저 오른쪽 바에서 Device Manager 을 열어서 이미 만들어 놓은 가상 머신 하나를 선택하고 연필 모양의 수정 버튼을 크릭 합니다.
device manager
이제 설정 화면이 나오면 왼쪽 하단에 있는 show Advanced Settings을 클릭해서 열어 봅니다.
설정 화면
Camera 설정을 보면 Front 카메라에는 없지만, Back 카메라에는 VirtualScene 라는 옵션이 있습니다. 이 옵션이 선택되도록 수정하고 저장을 클릭합니다.
선택 사항은 다음과 같습니다.
None : 카메라가 없는 선택
VirtualScene : 시뮬레이션 환경에서 가상 카메라 사용
Emulated : 시뮬레이션 카메라 사용
Device : 호스트 컴퓨터 웹캠 또는 내장 카메라 사용
가상 카메라는 마치 카메라가 있는 것 처럼 가상공간을 촬영하는 시뮬레이션을 해 준다는 의미입니다. 이제 앱을 실행해서 어떤 동작을 하는지 살펴보겠습니다.
이미지 적용 방법
avd 을 실행합니다. 다음 오른쪽 제일 하단에 있는 햄버거 메뉴를 클릭하여 설정 화면으로 들어갑니다. camera 메뉴를 클릭합니다.
Virtual Sence Images 에는 wall과 table 두 개가 있는데 wall 은 벽면에 표시되는 이미지 이고 table 은 테이블 위에 표시 되는 이미지 을 설정 하는 부분입니다. 이미지 열기를 선택하여 보여 주고 싶은 이미지를 선택 하여 적용합니다.
주의할 것은 이미지 촬영 시 휴대폰이 세로가 되도록 해서 촬영된 이미지를 적용해 주세요. 그래야 아래 예시처럼 벽면에 정상적으로 된 이미지를 보여 줄 수 있습니다.
이미지 적용 순서
적용된 이미지는 가상공간에서 주방을 찾아가면 벽면과 테이블에 각각 선택한 이미지가 보입니다.
카메라 영상 샘플
이제 AVD에서 카메라를 열어 선택한 이미지가 나오는 곳을 찾아보도록 하겠습니다.
가상공간 탐색
AVD의 카메라에 보이는 가상공간을 탐색해 보겠습니다. AVD의 하단에 보면 alt 키를 이용하여 카메라를 이동할 수 있다는 표시가 나옵니다.
alt 키와 w (전진), s (후진), a (왼쪽으로), d (오른쪽으로) 등의 방향키를 이용하여 카메라의 시선을 옮겨 봅니다. 잘 찾아보면 거실에 있는 TV을 비추고 있던 카메라 안에 강아지 모양도 보이고, 주방으로 가는 공간도 보일 겁니다. 위 동영상 예시와 같이 말입니다.
alt 키를 누른 상태에서 마우스를 드래그해 보면 카메라의 앵글이 움직이는 것도 보실 수 있습니다.
AVD 카메라 모습
이와 같이 해서 주방 뒤에 있는 다이닝 룸(?)을 찾아가셨다면 벽면에 비치는 사진과 테이블 위에 비치는 사진을 보실 수 있습니다. 이렇게 해서 AVD에서 이미지 보여 주기 방법에 대하여 알아보았습니다.