오늘도 난 아무런 금전적 소득이 없다. 아직 뭐 한 달도 지나지 않았으니... 뭐 벌써 소득을 바라? 라고 말할 수 있을 것 같기는 하나... 그래도 ? 흠흠...
Gradle 7.4.0
오늘은 아침을 먹고 출근 하는 여보님을 배웅해 드리고, 난 또 앉아서 엊그제 올렸던 앱의 코드를 수정하고 있다. 어제 아침에 갑자기 android studio을 패치를 했더니, gradle 이 7.3.1에서 7.4.0으로 패치를 하게 됐다. 그래서 덩달아 gradle 파일에 implementation 선언했던 compose 버전도 1.4.0-alpha04 로 패치를 하란다.
아무 느낌 없이 그렇게 패치를 진행 했더니... 벌어지는 사태들... bottom navigation bar을 대신해 볼 요량으로 사용하기 위해서 implementation 했던 것이 오류를 내뱋기 시작했다. ㅠㅠ;;
Latest allowed version is 1.7.21 compatible with compose compiler 1.4.0-alpha02.
원작자님의 한 마디... 윽~~~ 병아리 코더는 이런 글을 볼 때 가슴이 찌져진다... 이 한 가지를 눈치채지 못해 하루 반나절이나 시간을 버렸으니 말이다.
patch 가 나온다고 해서 무작정 덤비는 (?) 것이 좋지 않다는 것을 배웠다. ㅋ~ 아무튼...
집을 나서다.
1인 개발자가 되어 좋은 건 아무때나, 아무 곳이나, 나의 사무실(?)이 되어 준다는 것이다. 오늘은 집을 나섰다. 앞에 말한 것처럼 여보님은 출근을 했고, 아들놈은 방학이라 아직 주무신다. 푸하하... 탈출을 해 보자... 집을 나서 길을 따라 오늘은 삼시로 정할 곳을 찾아본다.
지도보기
오늘은 너(?) 정했다. 코딩 작업은 잘 될 곳인가 ?
토닥토닥.... (키보드 두드리는 소리...) 왁자지껄 ( 주변 사람들 말소리...)
나만 이곳에 둥지(?)를 틀은 것은 아니고, 옆에 있는 고등학생(?)은 학원 가기 전에 자리를 잡고 있는지 연신 뭔가를 들여다보고 있고, 그 옆 대학생(?) 둘은 잡담이 길다.
어제 적용했던 패치를 복원했다... 다시 gradle 7.3.1로 돌리고 나머지 들만 patch을 적용해 테스트를 해 본다... 우쒸~~~
이제야 이전처럼 앱이 build 되고 실행도 된다. 음... 엊그제 올렸던 앱을 수정하고 다시 게시를 진행해 본다.
저녁 여섯 시 삼분 전... 나도 이제 퇴근(?)이라는 걸 해야겠다.
오늘 한 일
1. 앱 수정해서 다시 올렸다.
2. 별다방에 앉아서 별그램에 글도 하나 게시했고
3. 이 글도 적어 보고 있다.
Empty
비워라... 언제가 채워질 그날을 기다리며... 너무 오래 걸리지 않았으면 좋겠는 데...
corea ( 라틴어 표기?)에 bill (청구서)를 내 볼까? 하는 의미 라면 너무 건창 한 가?
아무튼 오늘 부터 billcorea라는 이름의 개인 사업자가 되어 살아 보기로 했다.
25년 11개월 내가 다녔던 어느 회사의 근무 기간
그 이전에 다녔던 회사 2곳 과 젊은 날의 패기로 했던 창업기간 은 각각이 고작 3년을 넘겨 보지 못했던 거 같은데
이제 떠나야할 시간이 되어 가고 있다는 것을 느끼기 시작하면서 준비를 시작하기는 했지만, 이런저런 이유로 그 준비기간이 끝나지 않았지만, 다가온 퇴직
그리고 한달여는 조금 가볍게 놀았다. 그 사이에 문득 가보고 싶었던 울릉도 여행도 했고, 최근 3년여 동안 느꼈던 마음의 짐(?)도 내려놓았다.
한달을 넘게 쉼을 가졌더니, 이렇게 계속 쉼을 할 수 만은 없는 일이지 않은가 ?
사업자 등록증
오늘 첫날의 일과는 재활용 쓰레기를 버리는 것으로시작하였다.ㅋ~ 혼자 하는 일이니자유로울까? 혼자 하는 일이니 뭐가다를까?
사업자 등록증 내기
세무서에 가야 하나? 어떻게 해야하는지모른다. 그래서 일단은 구글님(?)에게물어보기로했다. 네이버나, 다음 등의 검색페이지에서도검색이 되기는 하겠으나, 개발자로 살다 보니 구글이 더 편하게다가온다. 국내 메인 포탈은 국내 뉴스가 먼저 들어오기 때문에 헛 눈길을 많이 하게 된다. 아무튼...
검색정보
사업자 등록은 검색된 내용으로 국세청 홈텍스를 이용하면 어렵지 않게 등록을 할 수 있었다. 다만, 업태 / 종목을 정하는 문제가 있기는 하나 그것도 검색된 내용 등을 참고해서 종목을 결정하면 업태는 홈텍스에서 관리되는 것으로 해소가 되었다.
업태 및 종목
컴퓨터 프로그래밍 서비스업 : 이 건 용역 계약을 통해 프로그램 개발 및 유지보수 업무를 하기 위해서
응용 소프트웨어 개발 및 공급업 : 이 건 크몽, 사람인긱 등에서 앱 개발자로 살아 보기 위해서
광고 대행업 : 이 건 애드센스나 애드몹 같은 광고 플랫폼을 이용해 보기 위해서
3가지 의 종목이 선택되었고, 그대로 등록하였다.
사업장을 가져야 하는 가?
난 1인 개발자인데, 사업장이 필요한가? 우리 집 책방으로 사용하는 곳이 사업장이다. 그래서 사업자 등록할 때 주소동일 여부를 '여'로 선택하고 집 주소를 입력해 주었다.
관련 문서 제출
사업장이 있거나, 동업자가 있는 경우 제출하여야 하는 문서들이 있지만, 난 1인 사업자이니, 문서 등으로 제출해야 하는 페이지에서는 아무것도 제출하지 않고 다음으로 넘어갔다.
사업자용 계좌 개설은?
사용자용 계좌 개설은 필요한가에 대해서도 고민이 들었다. 그것도 필요는 해 보인다. 세무 신고등에 기초 자료로 사용하기 위해서는 말이다.
그리고 특히 일반과세로 사업자등록을 한 경우에는 필요할 듯해 보인다. 그래서 통장 개설을 새로 해야 하는지에 대한 고민이 생겼는 데, 일단은 홈텍스에서 신고/납부 탭에서 사업용 계좌 개설을 들어가 보았다니, 기존에 개설된 계좌를 등록할 수 있었다.
사업용 계좌 개설 관리
짠... 이제 준비는 되었다. 2023년 상반기 세무 신고를 해야 하는 시점까지 얼마 큼의 수익을 낼 수 있는 가는 모르겠지만...
이제 프리랜서 활동을 했던, 크몽 등의 사이트에 사업자로 전환 신청을 진행해야겠다.
고사상 ?
옛 어른들은 무언가를 시작할 때 고사를 지내는 관습이 있었는 데... 난 그냥 인터넷에서 펌(?) 한 사진 이미지 하나로 대신해 보겠다.
#스하리1000명프로젝트, Иногда сложно разговаривать с иностранными работниками, правда? Я сделал простое приложение, которое помогает! Вы пишете на своем языке, а другие видят это на своем. Он автоматически переводит в зависимости от настроек. Очень удобно для легкого общения. Посмотрите, когда будет возможность! https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416
JobScheduler 관리를 위해서 다음과 같이 선언해 둡니다. jobId 가 있어야 하기 때문이기도 하고, 반복 간격을 지정하기 위한 상수도 선언합니다. 그리고 성공 여부를 확인 하기 위한 상수도 같이 선언 합니다.
private val JOB_ID = 123 private var PERIODIC_TIME: Long = 15 * 60 * 1000 private val SUCCESS_KEY = "SUCCESS" private val FAILED_KEY = "FAILED"
알림 설정을 위해서는 다음과 같이 알림 매니저와 알림 실행 시 사용한 intent 변수 하나를 설정해 둡니다.
private var alarmMgr: AlarmManager? = null private lateinit var alarmIntent: PendingIntent
JobScheduler의 시작은 다음과 같이 코드 작업을 합니다. jobServices는 반복 작업에서 실행시킬 class 이름입니다.
setPersisted : 부팅된 이후에도 반복을 하게 할지 여부를 선택합니다.
setPeriodic : 반복 시간을 mS 단위로 설정하게 됩니다.
다른 선택 값들도 있으니 참고해서 살펴보세요.
val componentName = ComponentName(this, jobServices::class.java) val info = JobInfo.Builder(JOB_ID, componentName) .setPersisted(true) // true 부팅된 이후에도 반복하게 .setPeriodic(PERIODIC_TIME) // 지연시간 .build()
val jobScheduler: JobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler val resultCode = jobScheduler.schedule(info)
@SuppressLint("SpecifyJobSchedulerIdRange") class jobServices : JobService() {
companion object { private val TAG = "MyJobService" lateinit var sp: SharedPreferences }
@RequiresApi(Build.VERSION_CODES.S) override fun onStartJob(params: JobParameters): Boolean { var strDate = System.currentTimeMillis() var sdf = SimpleDateFormat("yyyy-MM-dd kk:mm:ss", Locale("ko", "KR")) var now = sdf.format(strDate ) var context = this@jobServices
#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시간 이상 걸리는 경우가 있으므로 미리 정기결제 항목을 등록해 두고 앱을 만들어 가는 것이 시간 활용에 도움이 됩니다.