2026/03/14

오늘의 이야기

오늘 일과


어제 playstore에 올렸던 앱이 버벅 거린다... 


우연히 사장 앱 수정중에...



 


오늘도 난 아무런 금전적 소득이 없다.  아직 뭐 한 달도 지나지 않았으니...  뭐 벌써 소득을 바라? 라고 말할 수 있을 것 같기는 하나... 그래도 ? 흠흠...


 


Gradle 7.4.0


오늘은 아침을 먹고 출근 하는 여보님을 배웅해 드리고, 난 또 앉아서 엊그제 올렸던 앱의 코드를 수정하고 있다.  어제 아침에 갑자기 android studio을 패치를 했더니, gradle 이 7.3.1에서 7.4.0으로 패치를 하게 됐다. 그래서 덩달아 gradle 파일에 implementation 선언했던 compose 버전도 1.4.0-alpha04 로 패치를 하란다.


 


아무 느낌 없이 그렇게 패치를 진행 했더니... 벌어지는 사태들...  bottom navigation bar을 대신해 볼 요량으로 사용하기 위해서 implementation 했던 것이 오류를 내뱋기 시작했다. ㅠㅠ;;


 


https://github.com/raamcosta/compose-destinations/issues/336



 


io.github.raamcosta.compose-destinations:core:1.8.32-beta upgrade refences ... · Issue #336 · raamcosta/compose-destinations


An error occurs while doing ./gradlew clean build. Why? Stack traces ./gradlew : e: C:\workspaces\Boss0426\app\src\main\java\com\billcoreatech\boss0426\CalcComposeActivity.kt: (34, 31): Unresolved ...


github.com




 


원작자님에게 질문을 해 본다. 꼬부랑 말로 다가... 요즘은 구글 번역이 글을 읽거나 번역을 하는 데 많은 도움을 주기 때문에 그렇게 어렵지 않게 꼬부랑 말을 해 볼 수 있다.  물론 나의 입으로 하는 것은 아니지만...


 


질문과 답하기



You cannot use kotlin 1.7.22


https://developer.android.com/jetpack/androidx/releases/compose-kotlin



 


Compose와 Kotlin의 호환성 지도  |  Android 개발자  |  Android Developers


Compose와 Kotlin의 호환성 지도 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 종속 항목 선언 Compose 컴파일러에 관한 종속 항목을 추가하려면 프로젝트에 Googl


developer.android.com




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 


비워라... 언제가 채워질 그날을 기다리며... 너무 오래 걸리지 않았으면 좋겠는 데...





오늘의 이야기

빌코리아 ( billcorea )


corea ( 라틴어 표기?)에 bill (청구서)를 내 볼까? 하는 의미 라면 너무 건창 한 가? 


아무튼 오늘 부터 billcorea라는 이름의 개인 사업자가 되어 살아 보기로 했다.


 


25년 11개월 내가 다녔던 어느 회사의 근무 기간


그 이전에 다녔던 회사 2곳 과 젊은 날의 패기로 했던 창업기간 은  각각이 고작 3년을 넘겨 보지 못했던 거 같은데


 


이제 떠나야할 시간이 되어 가고 있다는 것을 느끼기 시작하면서  준비를 시작하기는 했지만, 이런저런 이유로 그 준비기간이 끝나지 않았지만, 다가온 퇴직


 


그리고 한달여는 조금 가볍게 놀았다. 그 사이에 문득 가보고 싶었던 울릉도 여행도 했고, 최근 3년여 동안 느꼈던 마음의 짐(?)도 내려놓았다.


 


한달을 넘게 쉼을 가졌더니, 이렇게 계속 쉼을 할 수 만은 없는 일이지 않은가 ?


 


사업자 등록증



 


오늘 첫날의 일과는 재활용 쓰레기를 버리는 것으로 시작하였다. ㅋ~  혼자 하는 일이니 자유로울까?  혼자 하는 일이니 뭐가 다를까?


 


사업자 등록증 내기


세무서에 가야 하나? 어떻게 해야 하는지 모른다. 그래서 일단은 구글님(?)에게 물어보기로 했다. 네이버나, 다음 등의 검색 페이지에서도 검색이 되기는 하겠으나, 개발자로 살다 보니 구글이 더 편하게 다가온다.  국내 메인 포탈은 국내 뉴스가 먼저 들어오기 때문에 헛 눈길을 많이 하게 된다. 아무튼...


검색정보



 


사업자 등록은 검색된 내용으로 국세청 홈텍스를 이용하면 어렵지 않게 등록을 할 수 있었다.  다만, 업태 / 종목을 정하는 문제가 있기는 하나 그것도 검색된 내용 등을 참고해서 종목을 결정하면 업태는 홈텍스에서 관리되는 것으로 해소가 되었다.


 


업태 및 종목



  • 컴퓨터 프로그래밍 서비스업 : 이 건 용역 계약을 통해 프로그램 개발 및 유지보수 업무를 하기 위해서

  • 응용 소프트웨어 개발 및 공급업 : 이 건 크몽, 사람인긱 등에서 앱 개발자로 살아 보기 위해서

  • 광고 대행업 : 이 건 애드센스나 애드몹 같은 광고 플랫폼을 이용해 보기 위해서


3가지 의 종목이 선택되었고, 그대로 등록하였다.


 


사업장을 가져야 하는 가?


난 1인 개발자인데, 사업장이 필요한가?  우리 집 책방으로 사용하는 곳이 사업장이다. 그래서 사업자 등록할 때  주소동일 여부를 '여'로 선택하고 집 주소를 입력해 주었다.


 


관련 문서 제출


사업장이 있거나, 동업자가 있는 경우 제출하여야 하는 문서들이 있지만, 난 1인 사업자이니, 문서 등으로 제출해야 하는 페이지에서는 아무것도 제출하지 않고 다음으로 넘어갔다.


 


사업자용 계좌 개설은?


사용자용 계좌 개설은 필요한가에 대해서도 고민이 들었다.  그것도 필요는 해 보인다. 세무 신고등에 기초 자료로 사용하기 위해서는 말이다.


 


그리고 특히 일반과세로 사업자등록을 한 경우에는 필요할 듯해 보인다.  그래서 통장 개설을 새로 해야 하는지에 대한 고민이 생겼는 데, 일단은 홈텍스에서 신고/납부 탭에서 사업용 계좌 개설을 들어가 보았다니, 기존에 개설된 계좌를 등록할 수 있었다.


 


사업용 계좌 개설 관리



 


짠... 이제 준비는 되었다. 2023년 상반기 세무 신고를 해야 하는 시점까지 얼마 큼의 수익을 낼 수 있는 가는 모르겠지만...


 


이제 프리랜서 활동을 했던, 크몽 등의 사이트에 사업자로 전환 신청을 진행해야겠다.


 


고사상 ?


옛 어른들은 무언가를 시작할 때 고사를 지내는 관습이 있었는 데... 난 그냥 인터넷에서 펌(?) 한 사진 이미지 하나로 대신해 보겠다.


고사상 차림 인터넷 펌



 





오늘의 이야기


#스하리1000명프로젝트,
Иногда сложно разговаривать с иностранными работниками, правда?
Я сделал простое приложение, которое помогает! Вы пишете на своем языке, а другие видят это на своем.
Он автоматически переводит в зависимости от настроек.
Очень удобно для легкого общения. Посмотрите, когда будет возможность!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

 


손에 들고 다니는 스마트폰에 무슨 일을 그렇게 시켜 먹을라고(?) 이런 것들이 있는 가? 하는 생각이 들 무렵입니다. 


그래도 우린 이제 이런 배치(반복작업을 위한) 처리를 해야 하는 경우가 있어서 이런 것들에 대해서 알아 보고자 합니다. 


 



  • Alarm manager

  • Job Scheduler

  • Worker


반복적인 일을 시키는 방법 3가지를 살펴 보고자 합니다.


Alarm manager


알림은 지정한 시간에 어떤 이벤트가 있는 지를 알려주는 역할을 하게 됩니다.  그것들 중에서 1회성 알림만을 사용하는 경우도 있기는 하겠지만,  앱을 개발하는 경우에는 반복적인 알림을 발생시키는 것이 좋을 때가 있습니다.  그때 사용을 하게 될 것 같습니다. 


 


알림 매니저가 좋은 건 15분 미만의 경우도 반복 작업을 할 수 있다는 점 입니다.


https://developer.android.com/reference/android/app/AlarmManager



 


AlarmManager  |  Android Developers


 


developer.android.com




 


Job Scheduler


 


이름 그대로 일을 스케줄에 맞게 반복적인 작업을 실행하는 경우에 사용하게 될 것 같습니다. 다만,  안드로이드 버전이 높아지면서 배터리 효율을 관리하기 위해 최소 시간이 15분이라는 간격을 유지해야 한다는 불편함(?)이 있다는 것이 아쉽게 다가올 뿐입니다. 


 


https://developer.android.com/reference/android/app/job/JobScheduler



 


JobScheduler  |  Android Developers


 


developer.android.com




 


Worker


비동기식 작업을 설정하는 방법 중에 하나라고는 들었으나, 아직 아는 바가 없어 이 부분에 대한 기술은 훗날로 미루어 봅니다. ㅋ~


https://developer.android.com/reference/androidx/work/Worker



 


Worker  |  Android Developers


androidx.constraintlayout.core.motion.parse


developer.android.com




 


구현해 보기


이제 코드로 하나씩 만들어 보겠습니다.  


 


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)

val isJobScheduledSuccess = resultCode == JobScheduler.RESULT_SUCCESS
Log.e(TAG, "Job Scheduled ${if (isJobScheduledSuccess) SUCCESS_KEY else FAILED_KEY}")

 


알림의 경우도 알림 상수로 선언한 알림 매니저에 알림 실행 시 사용할 AlarmReceiver을 선언하고  알림 설정을 시작합니다.  반복 작업을 할 예정이기 때문에 Repeting 함수들 중에서 골랐습니다.  반복이 아닌 1회성의 경우는 다른 함수를 사용하게 됩니다. 



  • setInexactRepeating : 알림 시간 이후 반복 시간 동안 절전모드를 해제하여 알림을 발생시킵니다.

  • set : 시정된 시간에 알림을 1회성으로 절전모드를 해제하여 알림을 발생시킵니다.


alarmMgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmIntent = Intent(applicationContext, AlarmReceiver::class.java).let { intent ->
PendingIntent.getBroadcast(applicationContext, 0, intent, FLAG_MUTABLE)
}

alarmMgr?.setInexactRepeating(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + sp.getFloat("repeatTerm", 1f).toLong() * 60 * 1000, // 1분 단위로
sp.getFloat("repeatTerm", 1f).toLong() * 60 * 1000,
alarmIntent
)

 


이제 코드 작업을 시작하였으니 작업 해제 하는 코드를 살펴보겠습니다. 


 


JobScheduler의 경우는 앞에서 시작할 때 지정했던 jobID을 이용하여 반복 작업을 하는 하는 반면에...


val jobScheduler: JobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
jobScheduler.cancel(JOB_ID)
Log.e(TAG, "Job CANCELED")

 


알림의 경우는 알림 매니저를 그냥 cancel 하는 것으로 작업이 종료됩니다.


alarmMgr?.cancel(alarmIntent)

 


이제 각 배치 작업에서 사용할 Service의 코드를 보겠습니다. 


먼저 manifest.xml 파일에 등록한 부분을 보겠습니다. 


<receiver android:name=".receivers.AlarmReceiver"
android:exported="false"/>

<service
android:name=".services.jobServices"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />

알림을 위해서는 receiver로 등록을 하게 되고, jobscheduler을 위해서는 jobservices로 등록을 하게 됩니다.  각 class의 전체 코드는 다음과 같습니다. 


 


JobServices 


import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.billcoreatech.smsreceiver1113.BuildConfig
import com.billcoreatech.smsreceiver1113.MainActivity
import com.billcoreatech.smsreceiver1113.R
import com.billcoreatech.smsreceiver1113.retrofit.RetrofitService
import java.text.SimpleDateFormat
import java.util.*

@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

sp = getSharedPreferences(packageName, MODE_PRIVATE)

Log.e(TAG, "${now} onStartJob: ${params.jobId} ${sp.getFloat("repeatTerm", 1f).toLong()} ${sp.getBoolean("jobScheduled", false)}")
sendNotification(this@jobServices, "Activated ...")
return false
}

override
fun onStopJob(params: JobParameters): Boolean {
Log.e(TAG, "onStopJob: ${params.jobId}")
return false
}

@RequiresApi(Build.VERSION_CODES.S)
@SuppressLint("MissingPermission")
private fun sendNotification(context : Context, messageBody: String) {
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val extras = Bundle()
extras.putString("MSGRCV", messageBody)
intent.putExtras(extras)
val pendingIntent = PendingIntent.getActivity(
context, 0 /* Request code */, intent,
PendingIntent.FLAG_MUTABLE
)

val channelId: String = context.getString(R.string.default_notification_channel_id)
val channelName: CharSequence = context.getString(R.string.default_notification_channel_name)
val importance = NotificationManager.IMPORTANCE_HIGH
val notificationChannel = NotificationChannel(channelId, channelName, importance)
notificationChannel.enableLights(true)
notificationChannel.lightColor = Color.RED
notificationChannel.enableVibration(true)
notificationChannel.vibrationPattern =
longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)

val wearNotifyManager = NotificationManagerCompat.from(context)
val wearNotifyBuilder: NotificationCompat.Builder =
NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_outline_sms_24)
.setContentTitle(context.getString(R.string.app_name))
.setContentText(messageBody)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setVibrate(longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400))
.setDefaults(-1)

wearNotifyManager.createNotificationChannel(notificationChannel)
wearNotifyManager.notify(0, wearNotifyBuilder.build())
}
}

 


AlarmReceiver



import android.app.AlarmManager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.SystemClock
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import com.billcoreatech.smsreceiver1113.MainActivity
import com.billcoreatech.smsreceiver1113.R

class AlarmReceiver : BroadcastReceiver() {

companion object {
const val TAG = "AlarmReceiver"
const val NOTIFICATION_ID = 0
const val PRIMARY_CHANNEL_ID = "primary_notification_channel"
}

lateinit var notificationManager: NotificationManager

@RequiresApi(Build.VERSION_CODES.S)
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Received intent : $intent")
notificationManager = context.getSystemService(
Context.NOTIFICATION_SERVICE) as NotificationManager

createNotificationChannel(context)
deliverNotification(context)

}

@RequiresApi(Build.VERSION_CODES.S)
private fun deliverNotification(context: Context) {
val contentIntent = Intent(context, MainActivity::class.java)
val contentPendingIntent = PendingIntent.getActivity(
context,
NOTIFICATION_ID,
contentIntent,
PendingIntent.FLAG_MUTABLE
)
var sp = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)

val builder =
NotificationCompat.Builder(context, PRIMARY_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_outline_sms_24)
.setContentTitle(context.getString(R.string.app_name))
.setContentText("This is repeating alarm repeatTerm ${sp.getFloat("repeatTerm", 1f).toLong()} min")
.setContentIntent(contentPendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)

Log.e("", "This is repeating alarm repeatTerm ${sp.getFloat("repeatTerm", 1f).toLong()} min")

notificationManager.notify(NOTIFICATION_ID, builder.build())
}

private fun createNotificationChannel(context: Context) {

val notificationChannel = NotificationChannel(
PRIMARY_CHANNEL_ID,
context.getString(R.string.app_name),
NotificationManager.IMPORTANCE_HIGH
)
notificationChannel.enableLights(true)
notificationChannel.lightColor = R.color.softRed
notificationChannel.enableVibration(true)
notificationChannel.description = context.getString(R.string.app_name)
notificationManager.createNotificationChannel(
notificationChannel)

}
}

 


두 가지 케이스 모두 그냥 job에서는 Notification을 발생시키는 것이 목적인 코드입니다.  이제 코드작성 방법은 다 모았으니 다른 작업으로 발전해 나갈 수 있겠지요?


 


 





오늘의 이야기

onBackPressed 가 deprecated 되었다?


이제 우리는 구글이 제안하는 안드로이드 13에 타기팅하는 앱을 제출 해야만 하는 시기에 도달하고 있습니다.  구글이 새로운 안드로이드 버전을 배포하기 시작하면서 오래된 안드로이드에 대한 게시를 제한 합니다. 


 


그래서 이번에 API 33 인 안드로이드 13에 타겟팅 하는 앱을 작성해 보았습니다. 그러다 만난 몇 가지 사용 제한이 되는 것들에 대한 정리를 해 두고자 합니다. 


 


onBackPressed는 사용자가 뒤로 가기 버튼을 클릭하는 경우 제어를 하기 위해서 사용했던 함수 입니다. MainActivity 에서 최종적으로 뒤로 가기를 클릭 하는 경우 앱을 종료시키는 기능도 사용이 되는 함수였는 데...


 


안드로이드 13에서는 더 이상 사용할 수 없는 (?)  - 사용은 가능 하나 소스 코드에 중간 줄이 생긴 모양을 보면서 코드를 지속적으로 봐야 합니다. 


 


onBackPressed



어떻게 해소를 하면 될까요?


 


CallBack을 하나 만들어 봅니다.


private val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
doCloseApps()
}
}

다른 건 없고 이런 모양으로 callback 함수를 하나 만들어 둡니다.  그러고 onCreate 에서 이 callback 이 호출 되도록 한 줄 넣어 주는 것으로 그 코딩은 마무리 됩니다. 


 


@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

this.onBackPressedDispatcher.addCallback(this, callback)


}

그리고 나서 callback에서 처리하고자 하는 코드를 작성하는 것으로 끝입니다. 


 


간단하죠?





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle — незаменимое приложение для бадминтонных клубов!
👉 Матчевая игра: записывайте результаты и находите противников 🎉
Идеально подходит для любого места: в одиночку, с друзьями или в клубе! 🤝
Если вам нравится бадминтон, обязательно попробуйте

Зайди в приложение 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

구글 클라우드 함수 설정 


서버 없는 개발자여... 이제 당신도 서버의 역할을 구성할 수 있습니다.  이 글은 아래 개발자 가이드를 참고하여 작성했습니다.


 


https://cloud.google.com/functions/docs/create-deploy-http-python?hl=ko#windows 



 


빠른 시작: Python을 사용하여 HTTP Cloud 함수 만들기 및 배포  |  Cloud Functions 문서  |  Google Cloud


의견 보내기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 빠른 시작: Python을 사용하여 HTTP Cloud 함수 만들기 및 배포 Python을 사용하여 HTTP Cloud 함수 만들기


cloud.google.com




 


1. 프로젝트 선택


이미 구성해 놓은 여러 개의 프로젝트 중에서 사용할 프로젝트를 선택하는 과정입니다.


프로젝트 선택



 


2. API 사용 설정


다음 해당 프로젝트에서 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

Network diagnostic detects and fixes local network connection issues.
Checking network connection...done.
Reachability Check passed.
Network diagnostic passed (1/1 checks passed).

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.

 


5. python 설정을 시작합니다.


https://cloud.google.com/python/docs/setup?hl=ko 



 


Python 개발 환경 설정  |  Google Cloud


의견 보내기 Python 개발 환경 설정 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 가이드에서는 Google Cloud에서 실행되는 Python 앱 개발을 포함하여 Python 개


cloud.google.com




개발 가이드의 내용을 보고 따라 해 봅니다.


먼저 project 가 들어 있는 폴더로 이동합니다.  다음 실행 명령어 다음과 같습니다.



  • py -m pip --version : pip 버전은 항상 최신을 유지하도록 해야 합니다.  python.exe -m pip install --upgrade pip을 실행해서 최신 버전이 설치되도록 한 다음 진행 하면 좋습니다.

  • py -m venv env : 이제 개별 환경을 위한 가상 환경을 구성합니다.

  • .\env\Scripts\actvate : 구성된 가상 환경에서 스크립트를 실행해 화면을 활성화합니다.

  • pip install google-cloud-storage : 가상 환경에 cloud 함수 실행을 위한 라이브러리를 설치합니다.


PS C:\workspaces\cloudhome\boss0426> py -m pip --version
pip 22.3.1 from C:\Users\nari4\AppData\Roaming\Python\Python311\site-packages\pip (python 3.11)
PS C:\workspaces\cloudhome\boss0426> py -m venv env
PS C:\workspaces\cloudhome\boss0426> .\env\Scripts\activate
(env) PS C:\workspaces\cloudhome\boss0426>
(env) PS C:\workspaces\cloudhome\boss0426>
(env) PS C:\workspaces\cloudhome\boss0426>
(env) PS C:\workspaces\cloudhome\boss0426> pip install google-cloud-storage
Collecting google-cloud-storage
Downloading google_cloud_storage-2.7.0-py2.py3-none-any.whl (110 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 110.2/110.2 kB 6.7 MB/s eta 0:00:00
Collecting google-auth<3.0dev,>=1.25.0
Downloading google_auth-2.15.0-py2.py3-none-any.whl (177 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 177.0/177.0 kB 10.4 MB/s eta 0:00:00
Collecting google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0dev,>=1.31.5
Downloading google_api_core-2.11.0-py3-none-any.whl (120 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 120.3/120.3 kB 6.9 MB/s eta 0:00:00
Collecting google-cloud-core<3.0dev,>=2.3.0
Using cached google_cloud_core-2.3.2-py2.py3-none-any.whl (29 kB)
Collecting google-resumable-media>=2.3.2
Using cached google_resumable_media-2.4.0-py2.py3-none-any.whl (77 kB)
Collecting requests<3.0.0dev,>=2.18.0
Using cached requests-2.28.1-py3-none-any.whl (62 kB)
Collecting googleapis-common-protos<2.0dev,>=1.56.2
Downloading googleapis_common_protos-1.57.0-py2.py3-none-any.whl (217 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 218.0/218.0 kB 13.0 MB/s eta 0:00:00
Collecting protobuf!=3.20.0,!=3.20.1,!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev,>=3.19.5
Downloading protobuf-4.21.12-cp310-abi3-win_amd64.whl (527 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 527.0/527.0 kB 11.0 MB/s eta 0:00:00
Collecting cachetools<6.0,>=2.0.0
Using cached cachetools-5.2.0-py3-none-any.whl (9.3 kB)
Collecting pyasn1-modules>=0.2.1
Using cached pyasn1_modules-0.2.8-py2.py3-none-any.whl (155 kB)
Collecting six>=1.9.0
Using cached six-1.16.0-py2.py3-none-any.whl (11 kB)
Collecting rsa<5,>=3.1.4
Using cached rsa-4.9-py3-none-any.whl (34 kB)
Collecting google-crc32c<2.0dev,>=1.0
Using cached google_crc32c-1.5.0-cp311-cp311-win_amd64.whl (27 kB)
Collecting charset-normalizer<3,>=2
Using cached charset_normalizer-2.1.1-py3-none-any.whl (39 kB)
Collecting idna<4,>=2.5
Using cached idna-3.4-py3-none-any.whl (61 kB)
Collecting urllib3<1.27,>=1.21.1
Downloading urllib3-1.26.13-py2.py3-none-any.whl (140 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 140.6/140.6 kB 8.1 MB/s eta 0:00:00
Collecting certifi>=2017.4.17
Downloading certifi-2022.12.7-py3-none-any.whl (155 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 155.3/155.3 kB ? eta 0:00:00
Collecting pyasn1<0.5.0,>=0.4.6
Using cached pyasn1-0.4.8-py2.py3-none-any.whl (77 kB)
Installing collected packages: pyasn1, urllib3, six, rsa, pyasn1-modules, protobuf, idna, google-crc32c, charset-normalizer, certifi, cachetools, requests, googleapis-common-protos, google-resumable-media, google-auth, google-api-core, google-cloud-core, google-cloud-storage
Successfully installed cachetools-5.2.0 certifi-2022.12.7 charset-normalizer-2.1.1 google-api-core-2.11.0 google-auth-2.15.0 google-cloud-core-2.3.2 google-cloud-storage-2.7.0 google-crc32c-1.5.0 google-resumable-media-2.4.0 googleapis-common-protos-1.57.0 idna-3.4 protobuf-4.21.12 pyasn1-0.4.8 pyasn1-modules-0.2.8 requests-2.28.1 rsa-4.9 six-1.16.0 urllib3-1.26.13

[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 되어 같이 실행이 되는 것으로 보입니다.


4. 이제 deploy을 해 보겠습니다.


 


    gcloud functions deploy 'function_name' --runtime python310 --trigger-http --allow-unauthenticated


(env) PS C:\workspaces\cloudhome\boss0426> gcloud functions deploy boss0426_request --runtime python310 --trigger-http --allow-unauthenticated
Created .gcloudignore file. See `gcloud topic gcloudignore` for details.
Deploying function (may take a while - up to 2 minutes)...⠛
For Cloud Build Logs, visit: https://console.cloud.google.com/cloud-build/builds;region=us-central1/0d6b660e-85d2-4e6d-86c9-eb09bfaac827?project=319191239543
Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
buildId: 0d6b660e-85d2-4e6d-86c9-eb09bfaac827
buildName: projects/319191239543/locations/us-central1/builds/0d6b660e-85d2-4e6d-86c9-eb09bfaac827
dockerRegistry: CONTAINER_REGISTRY
entryPoint: boss0426_request
httpsTrigger:
securityLevel: SECURE_ALWAYS
url: https://us-c**********0.cloudfunctions.net/boss0426_request
ingressSettings: ALLOW_ALL
labels:
deployment-tool: cli-gcloud
name: projects/boss******0/locations/us-central1/functions/boss0426_request
runtime: python310
serviceAccountEmail: bo*******ppspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/uploads-103870777877.us-central1.cloudfunctions.appspot.com/6f47b5ef-c5ef-4163-843d-08c8a48fa10a.zip
status: ACTIVE
timeout: 60s
updateTime: '2023-01-04T05:30:31.359Z'
versionId: '1'
(env) PS C:\workspaces\cloudhome\boss0426>

이제 설정이 완료되었습니다.


 


5. cloud console에서 로그 활동을 확인해 보겠습니다.


콘솔 에서 로그 보기



 


설정도 되었고 호출해보니 호출도 되는 것 같습니다.  이제 실제 앱에서 구동을 해서 확인해 보아야 할 차례입니다.


그 뒷 이야기는 다음에...


 


 





오늘의 이야기

인앱 결제 버전이 올라갔어요



알림: 2022년 8월 2일부터 모든 신규 앱은 결제 라이브러리 버전 4 이상을 사용해야 합니다. 2022년 11월 1일부터는 기존 앱의 모든 업데이트에도 결제 라이브러리 버전 4 이상이 요구됩니다.

구글에서 가이드하는 내용입니다. 이제 구글 인앱 결제로 라이브러리를 업데이트해야 할 것 같아요. 수수료도 30%나 떼어 가면서 요구하는 것도 많습니다. 그래도 어쩌겠어요. 장터(?)가 그것 밖에는 없으니 말이죠.

https://developer.android.com/google/play/billing/migrate-gpblv5?hl=ko







Google Play 결제 라이브러리 4에서 5로의 이전 가이드  |  Google Play 결제 시스템  |  Android Developers


알림: 2022년 8월 2일부터 모든 신규 앱은 결제 라이브러리 버전 4 이상을 사용해야 합니다. 2022년 11월 1일부터는 기존 앱의 모든 업데이트에도 결제 라이브러리 버전 4 이상이 요구됩니다. 자세히


developer.android.com





https://qonversion.io/blog/google-play-billing-library-5-0/







Google Play Billing Library 5.0 overview: new subscription model


Google introduced its new major version of the Google Play Billing Library on recent I/O conference, which include vast information about the new architecture of subscriptions. Let's explore these updates in this article.


qonversion.io





이전 버전에서 사용되던 함수가 더 이상 사용할 수 없게 되면서 이전이 필요해졌습니다. 이전 가이드에 표시되어 있는 것처럼 정리가 일부 필요해 보입니다. 아무튼 이런 정보들을 이용해서 다음 버전으로 이전을 시작해 봅니다.


정기결제 정책도 새로 추가 ?


개발 가이드를 읽어 보면 새로운 API을 활용하기 위해서는 새로운 정기결제 항목이 필요해 보입니다. 새로 등록을 해 보도록 하겠습니다.

정기결제 만들기


콘솔에서 수익 창출 -> 정기 결제에 들어가서 새로운 구독 만들기를 눌러서 새롭게 하나 만들어 봅니다.
다만, 정기결제를 위해서는 앱은 무료로 등록 해야 합니다.

새로 추가한 정기 결제


여기 까지는 쉽게 따라오실 수 있습니다. 그런데 이제는 기본 요금제 및 혜택을 등록하도록 하고 있습니다. 그럼 새로운 기본 요금제를 설정해 보겠습니다.

기본 요금제 선택



  • 자동갱신

  • 선불


2가지의 선택이 있습니다. 자동 갱신은 고객이 해지하기 전까지 자동으로 갱신을 시도한다는 의미이고, 선불은 고객이 필요할 때마다 결제를 진행하여야 하는 경우입니다.

여기서 주의할 점은 선불의 경우는 혜택을 추가할 수 없었습니다. 그래서 추가적인 혜택을 부여하고자 하면 자동 갱신을 만들어 주어야 할 것 같습니다.

** 그리고 저장을 하려고 하면 문제는 출시하려는 국가별로 가격을 정해 주어야 한다는 것입니다. 그래서 처음에는 출시 국가를 줄이는 방법을 선택했다가, 찾아보니 일괄 설정이 가능했습니다.

방법은 화면에 있는 Set prices 클릭하는 것입니다.

전체 일괄 선택후 가격 설정 하기


그러면 국가 리스트가 나오고 일괄 선택도 가능 합니다. 이제 우리나라 통화인 KRW 기준으로 금액을 입력하고 update을 해 보겠습니다.

기준 금액 입력


각 나라별 금액 일괄 입력 후


이제 필요에 따라서 혜택을 추가해 볼 수 있겠습니다. 하지 않아도 되기는 하겠지만 그래도 한번 해 보겠습니다.

정기결제 설정후 상세



혜택추가1단계


id을 입력하고 자격 기준을 선택해 봅니다. 3가지의 선택 사항이 존재하는 데, 이번에는 신규 고객 획득의 경우만 선택해 보겠습니다.



  • 신규고객획득 : 새로 앱을 설치한 사용자에게만 적용됩니다.

  • 업그레이드 : 이전 정기결제를 하고 있는 사용자가 다른 선택을 하게 되는 경우 적용됩니다.

  • 개발자선택 : 개발자 가 선택 사항을 조합해서 적용할 수 있습니다.


단계 추가를 해야 합니다. 신규 고객에게 어떤 조건으로 혜택을 줄 것 인가를 정하게 되는 데, 무료 체험판 제공을 해 보겠습니다.



  • 무료 체험판 : 기간을 정해 놓고 무료 혜택을 제공할 수 있습니다.

  • 1회 결제 : 1회만 혜택을 제공할 수 있습니다.

  • 할인된 반복 결제 : 반복적으로 정해진 기간 동안 할인된 금액으로 결제할 수 있는 혜택을 제공할 수 있습니다.


이제 조건 입력이 되었으니 활성화를 클릭하면 시행됩니다.


앱에서 확인해 보기


이제 앱에서 정상적으로 동작을 하는지 확인해 볼 차례입니다. 음... 등록한 지 얼마 되지 않아서 일까요? 아직 목록이 나오지 않고 있습니다.

동작이 원활하게 되는지 보도록 하겠습니다.





인앱결제 실행 화면


이글과 관련해서 수정된 코드는 아래 github을 참고하세요.

https://github.com/nari4169/daycnt415_kotlin/blob/master/app/src/main/java/com/billcoreatech/daycnt415/billing/BillingManager.kt







GitHub - nari4169/daycnt415_kotlin


Contribute to nari4169/daycnt415_kotlin development by creating an account on GitHub.


github.com





연말이 되었습니다. 2023년에도 열공하는 여러분이 되시길 바랄게요.

이 글이 도움이 되셨다면... 아래 광고도 클릭 한번 부탁드려요. ^^;;





오늘의 이야기


#스하리1000명프로젝트,
迷失在韩国?即使您不会说韩语,这个应用程序也可以帮助您轻松出行。
只需说出您的语言即可 - 它会翻译、搜索并以您的语言显示结果。
非常适合旅行者!支持英语、日语、中文、越南语等10多种语言。
现在就试试吧!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/03/13

오늘의 이야기

인앱결제 코드 이전 이야기


이전 버전에서는 java 코드로 구현된 소스 코드를 공유해 보았습니다.


 


https://billcorea.tistory.com/165



 


안드로이드 앱 만들기 : 구글 인앱 결제 쉽게 따라하기 (정기결제, 소스공유)


이전 포스팅 이전에 작성했던 포스팅을 참고하여 인앱 결제를 구현했던 기억을 되살펴 보겠습니다. https://billcorea.tistory.com/27 안드로이드 앱 만들기 구글 인앱결제 쉽게 따라 하기... 인앱 결제를


billcorea.tistory.com




 


오늘은 이 코드를 그대로 kotlin  코드로 변환을 해 보았습니다. 


Java File to Kotlin File


Android Studio 에서는 java 코드를 kotlin으로 변환해 줍니다. 


메뉴에서 Code 제일 아래에 보면 Convert Java File to Kotlin File 이 보입니다.  물로 이 메뉴는 Java 코드일 때만 보입니다.


android studio 메뉴



 


변환을 시행해 보겠습니다. 변환은 내 앱의 상위 package 이름이 나와 있는 위치에서 오른쪽 마우스를 클릭해서 하는 방법도 있습니다.  개발 java 파일을 선택해서 오른쪽 마우스 클릭해서 하게 되는 경우는 개별 파일만 처리해 주지만, 최상위 package을 선택하고 하는 경우 하위 경로에 있는 모든 파일을 한 번에 변환해 줍니다. 


 


주의 사항  


일괄 변환된 후에 해야할 일들이 생깁니다. java 코드에서는 global 변수로 사용하고자 하는 경우 그냥 변수 이름만 선언해 주면 되었던 부분들이 kotlin을 변환하게 되면 그 값을 정해 주는 것에 대해서 설정을 해 주어야 하는 부분들이 생기며  해당 변수를 일괄적으로 null 대입하는 코드로 변환을 해 주시기 때문에 아래 예시들처럼 수정을 해 주어야 하는 부분들이 생깁니다.


 


변환 전 / 후



위 예시는 kotlin  으로 변환 후에 코드를 정리한 후의 코드이니 변환된 직후의 코드와는 다르다는 것을 염두에 두고 보시길 바랍니다. 


 


조금더 자세한 주의 사항을 보시려면 google  에서 제공하는 codelab 을 살펴 보세요.


https://codelabs.developers.google.com/codelabs/java-to-kotlin?hl=ko#1 



 


Kotlin으로 변환  |  Google Codelabs


이 Codelab에서는 자바 코드를 Kotlin으로 변환하는 방법을 알아봅니다.


codelabs.developers.google.com




 


Gradle 설정 추가


 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 추가
}

 


이제 빌드를 진행해 봅니다. 


 


앱 실행 화면



 


이런 게 변환된 코드는 정상적으로 실행이 되는 것을 확인했습니다. 


 


구글 인앱 결제 


인앱 정기결제 코드는 어떻게 변환이 되었을까요 ?


 



import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import android.widget.Toast
import com.android.billingclient.api.*
import com.billcoreatech.daycnt415.R
import com.billcoreatech.daycnt415.util.KakaoToast
import java.text.SimpleDateFormat
import java.util.*

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

init {
option = mActivity.getSharedPreferences("option", Context.MODE_PRIVATE)
editor = option.edit()
mBillingClient = BillingClient.newBuilder(mActivity)
.setListener(this)
.enablePendingPurchases()
.build()
mBillingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
Log.e(TAG, "respCode=" + billingResult.responseCode)
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
connectStatus = connectStatusTypes.connected
Log.e(TAG, "connected...")
purchaseAsync()
} else {
connectStatus = connectStatusTypes.fail
Log.i(TAG, "connected... fail ")
}
}

override fun onBillingServiceDisconnected() {
connectStatus = connectStatusTypes.disconnected
Log.i(TAG, "disconnected ")
}
})
}

/**
* 정기 결재 소모 여부를 수신 : 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시간 이상 걸리는 경우가 있으므로 미리 정기결제 항목을 등록해 두고 앱을 만들어 가는 것이 시간 활용에 도움이 됩니다. 


 


결제 테스트



 


이상으로 Java 코드의 소스를 Kotlin으로 변환을 해 보았습니다.


 


전체 코드 보기


전체소스코드는 아래 링크를 참고하세요.


https://github.com/nari4169/daycnt415_kotlin



 


GitHub - nari4169/daycnt415_kotlin


Contribute to nari4169/daycnt415_kotlin development by creating an account on GitHub.


github.com




 





오늘의 이야기

이런 앱도 만들어 보기는 했지만 필요한 경우가 있을까요? 알 수 없는 일이에요...