2026/03/01

오늘의 이야기

이 나라에는 여러 나라의 사람 들어와 살고 있다.  코리안 드림을 꿈꾸며... 한 때는 이 나라의 사람들이 아메리칸드림을 꿈꾸며, 미국으로 떠났던 것처럼... 그래서 이번에는 다국적 언어를 이용한 채팅앱을 하나 만들어 볼까 한다.


 


잘 될지는 모르겠지만, 그러면서 Jetpack Compose 을 이용한 화면 구성 등에 대한 공부를 해 볼 요량이다. 


오늘은 그 처음으로 로그인 페이지를 하나 만들어 보고자 한다. 


 


로그인 페이지



이 화면을 layout 으로 그리라고 했다면


아마도 금새 그리지 않았을까 하는 생각이 든다.


 


화면에 Card box 와 Text가 들어간 button 기능 3개


제목 이름 표시 하나


 


그런데, 난 이 화면 하나를 그리기 위해서 며칠을 고민했고, 예제를 하나 찾았다. 그리고 그 예제를 참고해 가면서 화면을 그렸다.


 


그러자니, 3시간쯤... 지났다.


 


예전 project 파일에서 필요한 기능 몇개를 가지고 왔고, 그리고는 이제 완성이 되었다.


 


그냥 layout 으로 그리는 것이 나은가?  쉽게 그리는 데는 그것이 낫을 것 같기도 하다.


 


 


 


 


 


 


 


 


 


 


 


화면을 그린 source 을 살펴보자.


 



import android.app.Activity
import android.content.Intent
import android.content.IntentSender
import android.content.SharedPreferences
import android.os.Bundle
import android.preference.PreferenceManager
import android.preference.PreferenceManager.getDefaultSharedPreferences
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.billcoreatech.multichat416.ui.theme.MultiChat416Theme
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.tasks.OnCompleteListener
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability
import com.google.android.play.core.review.ReviewInfo
import com.google.android.play.core.review.ReviewManager
import com.google.android.play.core.review.ReviewManagerFactory
import com.google.android.play.core.review.model.ReviewErrorCode
import com.google.android.play.core.tasks.RuntimeExecutionException
import com.google.android.play.core.tasks.Task
import com.google.firebase.appcheck.FirebaseAppCheck
import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory
import com.google.firebase.appcheck.safetynet.SafetyNetAppCheckProviderFactory
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.GoogleAuthProvider
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase
import java.text.SimpleDateFormat
import java.util.*

class GoogleLogin : ComponentActivity() {

var appUpdateManager: AppUpdateManager? = null
var appUpdateInfoTask: Task<AppUpdateInfo>? = null
lateinit var sp: SharedPreferences
lateinit var editor: SharedPreferences.Editor
lateinit var sdf: SimpleDateFormat
lateinit var today: Calendar
lateinit var googleSignInClient: GoogleSignInClient
lateinit var auth: FirebaseAuth
val MY_REQUEST_CODE = 1000
var RC_SIGN_IN:Int = 1001
var TAG = "GoogleLogin"

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val gso = GoogleSignInOptions
.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()

googleSignInClient = GoogleSignIn.getClient(this, gso)
auth = Firebase.auth

setContent {
MultiChat416Theme {

fun signIn() {
val signInIntent = googleSignInClient.signInIntent
startActivityForResult(signInIntent, RC_SIGN_IN)
}

fun doLogOut() {
googleSignInClient.signOut().addOnCompleteListener( OnCompleteListener {
task -> if (task.isComplete ) {
Log.e(TAG, "finish ... " )
auth.signOut()
finish()
}
})
}

Column(modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
){
Text(text = buildAnnotatedString {
withStyle(style = SpanStyle(color = Color.Red)) {
append("M")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("ulti")
}

withStyle(style = SpanStyle(color = Color.Red)) {
append(" L")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("anguage")
}

withStyle(style = SpanStyle(color = Color.Red)) {
append(" C")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("hatting")
}
}, fontSize = 30.sp)
Spacer(Modifier.size(32.dp))
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
signIn()
},content={
Text(text = getString(R.string.title_activity_google_login), color = Color.Blue)
})
}
Spacer(Modifier.size(8.dp))
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
Toast.makeText(applicationContext,
getString(R.string.mesgRegistorOnlyGoogle),
Toast.LENGTH_SHORT
).show()
}) {
Text(text = getString(R.string.doRegister), color = Color.Red)
}
}
Spacer(Modifier.size(8.dp))
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
doLogOut()
}) {
Text(text = getString(R.string.doLogout), color = Color.Gray)
}
}

}
}
}

val firebaseAppCheck = FirebaseAppCheck.getInstance()
firebaseAppCheck.installAppCheckProviderFactory(
if (BuildConfig.DEBUG_MODE) {
Log.e(TAG, "DebugAppCheckProviderFactory ...")
DebugAppCheckProviderFactory.getInstance() // AVD 에서 적용시
//SafetyNetAppCheckProviderFactory.getInstance() // 폰에서 적용시
} else {
Log.e(TAG, "SafetyNetAppCheckProviderFactory ...")
SafetyNetAppCheckProviderFactory.getInstance() // 폰에서 적용시
}
)

appUpdateManager = AppUpdateManagerFactory.create(this@GoogleLogin)
// Returns an intent object that you use to check for an update.
appUpdateInfoTask = appUpdateManager!!.appUpdateInfo
doUpdateCheck()

sp = getSharedPreferences("MultiChat", MODE_PRIVATE)
editor = sp.edit()

sdf = SimpleDateFormat("yyyyMMdd")
today = Calendar.getInstance()
if (!sp.getBoolean("REVIEW", false)) {
editor.putString("REVIEW_DATE", sdf.format(today.timeInMillis))
editor.commit()
}

// 사용하고 10일 지나가면 ...
if ((today.timeInMillis - sdf.parse(sp.getString("REVIEW_DATE",sdf.format(today.timeInMillis))).time) / (1000 * 60 * 60 * 24) > 10) {
var manager = ReviewManagerFactory.create(this@GoogleLogin)
var request = manager.requestReviewFlow()
editor.putBoolean("REVIEW", true)
editor.commit()
request.addOnCompleteListener { task ->
if (task.isSuccessful) {
// We got the ReviewInfo object
val reviewInfo = task.result
if (sp.getBoolean("REVIEW", false)) {
doReviseInfo(manager, this@GoogleLogin, reviewInfo)
}
} else {
// There was some problem, log or handle the error code.
@ReviewErrorCode val reviewErrorCode = (task.getException() as RuntimeExecutionException).errorCode
}
}
}
}

private fun doUpdateCheck() {
// Checks that the platform will allow the specified type of update.
appUpdateInfoTask!!.addOnSuccessListener { appUpdateInfo: AppUpdateInfo ->
if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
// This example applies an immediate update. To apply a flexible update
// instead, pass in AppUpdateType.FLEXIBLE
&& appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
) {
// Request the update.
try {
doRequestUpdate(appUpdateInfo)
} catch (e: IntentSender.SendIntentException) {
e.printStackTrace()
}
}
}
}

@Throws(IntentSender.SendIntentException::class)
private fun doRequestUpdate(appUpdateInfo: AppUpdateInfo) {
Log.e(TAG, "do Update Start")
appUpdateManager!!.startUpdateFlowForResult( // Pass the intent that is returned by 'getAppUpdateInfo()'.
appUpdateInfo, // Or 'AppUpdateType.FLEXIBLE' for flexible updates.
AppUpdateType.IMMEDIATE, // The current activity making the update request.
this, // Include a request code to later monitor this update request.
MY_REQUEST_CODE
)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

if (requestCode == MY_REQUEST_CODE) {
if (resultCode != RESULT_OK) {
Log.e(TAG, "Update flow failed! Result code: $resultCode")
// If the update is cancelled or fails,
// you can request to start the update again.
}
}

if (requestCode == RC_SIGN_IN) {
val task = GoogleSignIn.getSignedInAccountFromIntent(data)
try {
// Google Sign In was successful, authenticate with Firebase
val account = task.getResult(ApiException::class.java)!!
Log.e(TAG, "firebaseAuthWithGoogle: ${account.id}")
firebaseAuthWithGoogle(account.idToken!!)
} catch (e: ApiException) {
// Google Sign In failed, update UI appropriately
Log.e(TAG, "Google sign in failed ${e.message}")
}
}
}

private fun firebaseAuthWithGoogle(idToken: String) {
val credential = GoogleAuthProvider.getCredential(idToken, null)
auth.signInWithCredential(credential)
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
// Sign in success, update UI with the signed-in user's information
Log.e(TAG, "signInWithCredential:success")
val user = auth.currentUser
var intent = Intent(this@GoogleLogin, MainActivity::class.java)
startActivity(intent)
finish()
} else {
// If sign in fails, display a message to the user.
Log.w(TAG, "signInWithCredential:failure", task.exception)

}
}
}

private fun doReviseInfo(manager: ReviewManager, activity: Activity, reviewInfo: ReviewInfo) {
val flow = manager.launchReviewFlow(activity, reviewInfo)
flow.addOnCompleteListener { _ ->
editor.putBoolean("REVIEW", false)
editor.putString("REVIEW_DATE", sdf.format(today.timeInMillis))
editor.commit()
}
}
}

다른 건 볼 꺼 없고, setContent 함수에 들어 있는 것만 보면 될 것 같다.  전체 흐름을 알아야 하기 때문에,  


 


setContent {
// 테마이름은 개별적이므로 참고만...
MultiChat416Theme {

.....

// 먼저 아래로 배열을 하기 위해서 선언 linearlayout 처럼 사용...
Column(modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
){

// 제목을 표시 한다. 첫글짜만, 대문자로 표시, 폰트색상도 빨간색...
Text(text = buildAnnotatedString {
withStyle(style = SpanStyle(color = Color.Red)) {
append("M")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("ulti")
}

withStyle(style = SpanStyle(color = Color.Red)) {
append(" L")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("anguage")
}

withStyle(style = SpanStyle(color = Color.Red)) {
append(" C")
}
withStyle(style = SpanStyle(color = Color.Black)) {
append("hatting")
}
}, fontSize = 30.sp)
// 여백 두기
Spacer(Modifier.size(32.dp))

// button 을 구분하기 위해서 네모상자에 넣는다.
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
.....
},content={
Text(text = getString(R.string.title_activity_google_login), color = Color.Blue)
})
}

// 여백 두기
Spacer(Modifier.size(8.dp))
// button 을 구분하기 위해서 네모상자에 넣는다.
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
Toast.makeText(applicationContext,
getString(R.string.mesgRegistorOnlyGoogle),
Toast.LENGTH_SHORT
).show()
}) {
Text(text = getString(R.string.doRegister), color = Color.Red)
}
}

// 여백 두기
Spacer(Modifier.size(8.dp))
// button 을 구분하기 위해서 네모상자에 넣는다.
Card(modifier = Modifier
.width(200.dp)
.height(100.dp)
.padding(8.dp),
) {
TextButton(onClick = {
.....
}) {
Text(text = getString(R.string.doLogout), color = Color.Gray)
}
}

}
}
}

대략적으로 이런 정도의 코드로 위에 나오는 화면을 구성을 하게 된다.  이걸 Layout으로 만들어 보면 얼마나 걸릴까?


 


아무튼 오늘은 1일 차 coding 끝.


 


 


 


다음은 채팅목록을 구현해 보겠다.  Database 은 Firebase 의 Realtime database 을 활용할 생각이다.


 





오늘의 이야기

https://rrtutors.com/tutorials/implement-room-database-in-jetpack-compose



 


Flutter Mobile app,Flutter widgets,Jetpack Compose Tutorial,Create Android App,Java,python,Ruby


Create Flutter applications,Jetpack Compose Tutorial,Create android application,Python,Java,RxJava,dart,GoLang examples


rrtutors.com




 


사실은 room database 가 어떤 것으로 구성해야 하는 가에 대한 고민을 하는 과정에서 찾아보게 된 site인데, 


내용을 보던 중에 여러가지 예제를 볼 수 있었다.  샘플 코드를 보고 이것저것 해 보면 compose을 어떻게 구현해야 하는 가에 대한 이해를 할 수 있을 것 같아서 기록해 두기로 한다. 


 


어젯밤 이후에 확인된 부분만 간단하게 기록을 해 보면 다음과 같다. 


activity 연동 예제 1



 


activity 연동 예제 2




 




 


회원가입 페이지 예제



 


데이터를 이용한 list 보여주기



 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


 


이것들을 하나씩 찾아서 수정된 부분으로 실행을 해 보아야 하겠지만, 그런 정도의 수고(?)는 해야 배우는 맛이 나지 않겠는가?


 


코드 작업을 할 때 참고할 부분이 많을 것 같은 예제를 찾았다.  잘 뜯어 보면서 나의 코드를 만들어 보아야 겠다.


다음엔 나의 코드 이야기를 적어 볼 수 있기를 바라며...


 





오늘의 이야기



#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase

🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 "혹시나~" 하면서 봤거든 ㅋㅋ

근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨

그것도 구글 Gemini로다가!

그래서 앱 하나 만들었지
👉 "로또 예상번호 by Gemini" 🎱

AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨

재미로 해도 좋고…
혹시 모르는 거잖아? 😏


https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127




오늘의 이야기



4월도 끝나가는데...
봄을 부르는 소리라??? 스스로 생각해도 우습기는 하다.

봄이 완연한데... 더 좋은 일만 있으시길...





오늘의 이야기


#스하리1000명프로젝트

스치니들!
내가 만든 이 앱은, 내 폰에 오는 알림 중에서 중요한 키워드가 있는 경우
등록해둔 친구에게 자동으로 전달해주는 앱이야 📲

예를 들어, 카드 결제 알림을 와이프나 자녀에게 보내주거나
이번 달 지출을 달력처럼 확인할 수도 있어!

앱을 함께 쓰려면 친구도 설치 & 로그인해줘야 해.
그래야 친구 목록에서 서로 선택할 수 있으니까~
서로 써보고 불편한 점 있으면 알려줘 🙏

👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao





오늘의 이야기

어제는 오랜만에 갑천변을 걸었다. 길이 끝이 나지 않도록... 갑천을 걷고 있었는 데, 강가에 한쌍의 새가 있어서 사진을 찍어 보기로 했다.

내가 지금 사용하는 폰은 갤노트 20인데, 카메라가 30배까지의 줌을 지원한다. 그래서 그냥 30배로 확대해서 찍어 보았다.

한쌍의 새


한쌍의 새


일반적인 사진을 촬영할 때는 상관이 없었는데, 30배 줌으로 했더니, 흔들리기도 하고 사진이 명확하기 나오지 않는다. 이럴 때는 삼각대를 설치해야 하는 건가?

삼각대가 있었더면 이 보다는 조금 더 선명하게 촬영을 할 수 있었을까?

오늘도 날은 좋다. 창가 베란다에 않아서 이런 글을 적고 있는 지금은 밖의 기온은 따듯하다. 이 안이 덥게 느껴지는 것으로 봐서는...





오늘의 이야기

https://medium.com/@alexstyl/views-to-composables-d715b92c6055



 


From View to Composable: A Quick jump to Jetpack Compose from a Android View mindset (with Cheat…


Learn how to apply your View knowledge to speed up your Compose learning.


medium.com




 


오늘도 Jetpack Compose을 활용한 화면 구현에 대한 내용을 구글링을 통해서 보고 있는 데, 이런 정도의 자료가 있으면 도움이 될 듯하여 링크를 달아 둔다.


 


아직은 시작인 단계 이기 때문에 많은 정보를 찾아서 적어 두는 것이 나중에 배움을 구할 때 도움이 될 거 라고 생각된다.


 


 


compose cheat sheet 예시



 


하나씩 만들어 보자... 오늘도 파이팅!





오늘의 이야기


#스하리1000명프로젝트,
外国人労働者と話すのが難しいこともありますよね?
簡単に役立つアプリを作りました!あなたは自分の言語で書き、他の人は自分の言語でそれを見ます。
設定に基づいて自動翻訳します。
簡単なチャットに非常に便利です。機会があったら見てみてください!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

앞에서 python 코드를 이용해서 random 숫자를 만들고 FCM 전송하는 코드를 구현해 보았다면, 이번에 그걸 이용해서 수신하는 앱을 하나 만들어 볼 차례다.


 


https://billcorea.tistory.com/179



 


자작 앱 설명서 : 로또에 관심 있으세요?


https://play.google.com/store/apps/details?id=com.billcoreatech.getLotto Get Lotto 로또 번호를 드립니다. - Google Play 앱 매주 요청한 요일에 로또 번호를 무작위로 보내 드립니다. play.google.com 앱을..


billcorea.tistory.com




 


코드 구현은 kotlin으로 해 보았다.  이제 걸음마 단계이기 때문에 코드가 조금 길어질 수 도 있지만, 아직은 준비 중인 단계이기 때문에...


 


먼저 FCM을 수신하기 위해서는 firebase와 연동을 위한 gradle 구성이 필요하다.


 


import java.text.SimpleDateFormat

plugins {
id 'com.android.application'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
id 'kotlin-android-extensions'
id 'kotlin-android'
id 'kotlin-kapt'
}

android {
compileSdk 32

defaultConfig {
applicationId "com.bi.......tto"
minSdk 28
targetSdk 32
versionCode 10
versionName "0.1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
debug {
buildConfigField "Boolean", "DEBUG_MODE", "true"
}
release {
buildConfigField "Boolean", "DEBUG_MODE", "false"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

// jetPack compose add
buildFeatures {
viewBinding true
}

kotlinOptions {
jvmTarget = "1.8"
}

def archiveBuildType = ["release"]
applicationVariants.all { variant ->
variant.outputs.each { output ->
if (variant.buildType.name in archiveBuildType) {
def df = new SimpleDateFormat("yyyyMMdd")
df.setTimeZone(TimeZone.getDefault())
if (variant.versionName != null) {
String name = "GetLotto645_${df.format(new Date())}_${defaultConfig.versionCode}_${variant.versionName}.apk"
output.outputFileName = name
}
}
}
}
}

dependencies {

implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'com.google.android.play:core:1.10.3'

// 파이어 베이스 연동을 위한 설정...
implementation platform('com.google.firebase:firebase-bom:29.2.1')
// 메시징
implementation 'com.google.firebase:firebase-messaging:23.0.2'
// 인증처리
implementation 'com.google.firebase:firebase-auth-ktx'
implementation 'com.google.android.gms:play-services-auth:20.1.0'
// realtime database
implementation 'com.google.firebase:firebase-database-ktx'
// crashlytics
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-analytics-ktx'
// safetynet 앱 인증
implementation 'com.google.firebase:firebase-appcheck-safetynet:16.0.0-beta05'
implementation 'com.google.firebase:firebase-appcheck-debug:16.0.0-beta05'

implementation 'androidx.preference:preference-ktx:1.2.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation "androidx.core:core-ktx:1.7.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.android.gms:play-services-ads:20.6.0'

implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
implementation 'com.google.zxing:core:3.4.1'

}

 


다음은 manifest에 intenet 활용을 위한 permission 선언이 필요하고.


 


<uses-permission android:name="android.permission.INTERNET" />

그리고 service을 등록해야 한다. FCM 수신을 위한 리시버...


<service
android:name="com.billcoreatech.getLotto.utils.FcmReceiveService"
android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>

 


다음은 kotlin으로 구현한 service 코드 (사실 개발자 문서의 내용을 그대로 옮겨 왔다고 해도 과언은 아닐 것이다.)


package com.b...................to.utils

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color
import android.media.RingtoneManager
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import com.billcoreatech.getLotto.MainActivity
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import getLotto.R

class FcmReceiveService : FirebaseMessagingService() {

var TAG:String = "FcmReceiveService"

override fun onMessageReceived(remoteMessage: RemoteMessage) {

// TODO(developer): Handle FCM messages here.
// Not getting messages here? See why this may be: https://goo.gl/39bRNJ
Log.e(TAG, "From: " + remoteMessage.from)

// Check if message contains a data payload.
if (remoteMessage.data.size > 0) {
Log.e(TAG, "Message data payload: " + remoteMessage.data)
sendNotification(remoteMessage.data["body"])
var sp = getSharedPreferences("Messageing", MODE_PRIVATE)
var editor = sp.edit()
editor.putString("SendMsg", remoteMessage.data["body"]);
editor.putBoolean("msgSet", true)
editor.commit()
}

// Check if message contains a notification payload.
if (remoteMessage.notification != null) {
Log.e(
TAG, "Message Notification Body: " + remoteMessage.notification!!.body
)
sendNotification(remoteMessage.notification!!.body)
}
onDeletedMessages()
}

// [END receive_message]
override fun onNewToken(token: String) {
Log.e(TAG, "Refreshed token: $token")

// If you want to send messages to this application instance or
// manage this apps subscriptions on the server side, send the
// FCM registration token to your app server.
sendRegistrationToServer(token)
}
// [END on_new_token]
/**
* Handle time allotted to BroadcastReceivers.
*/
private fun handleNow() {
Log.d(TAG, "Short lived task is done.")
val intent = Intent(this@FcmReceiveService, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}

/**
* Persist token to third-party servers.
*
* Modify this method to associate the user's FCM registration token with any
* server-side account maintained by your application.
*
* @param token The new token.
*/
private fun sendRegistrationToServer(token: String) {
// TODO: Implement this method to send token to your app server.
}

/**
* Create and show a simple notification containing the received FCM message.
*
* @param messageBody FCM message body received.
*/
private fun sendNotification(messageBody: String?) {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
Log.e(TAG, "mesg=${messageBody}")

val pendingIntent = PendingIntent.getActivity(
this, 0 /* Request code */,
intent, PendingIntent.FLAG_IMMUTABLE
)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val channelId = getString(R.string.default_notification_channel_id)
val channelName: CharSequence = getString(R.string.default_notification_channel_name)
val importance = NotificationManager.IMPORTANCE_LOW
val notificationChannel = NotificationChannel(channelId, channelName, importance)
notificationChannel.enableLights(true)
notificationChannel.lightColor = Color.BLUE
notificationChannel.enableVibration(true)
notificationChannel.vibrationPattern =
longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
notificationManager.createNotificationChannel(notificationChannel)
val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val notificationBuilder = NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.mipmap.ic_logo645_foreground)
.setContentTitle(getString(R.string.fcm_message))
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri)
.extend(
NotificationCompat.WearableExtender()
.setBridgeTag("Foo")
.setContentIcon(R.mipmap.ic_logo645_foreground)
)
.setContentIntent(pendingIntent)

// Since android Oreo notification channel is needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"Channel human readable title",
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManager.createNotificationChannel(channel)
}

// Dismiss notification once the user touches it.
notificationBuilder.setAutoCancel(true)
notificationManager.notify(0 /* ID of notification */, notificationBuilder.build())
}

}

필요한 부분만 일부 수정을 했다.


 


이번에 또 알게 된 부분이라면 전송할 때 코드 구현에 따라 수신되는 위치가 달라진다는 것이다. 


 


<data로 구현했을 때>


def lambda_handler(token, context):
FCMToken = token

Data = {'data': {'title': 'Lotto 보내 드립니다.', 'body': context}, 'to': FCMToken}
Headers = {'Content-type': 'application/json',
'Authorization': 'Key=AAAAR_..........................3uTasJ-DGfJKZkS-ccyNr0xhRHTepcuk4GaFoNMTADl4jvNFM1HYIRqzSLs219BxVA-T9frSd3VCSUIRXXn1PSxhOKgqroBVqTaxmWk'}
http = urllib3.PoolManager().request('POST', 'https://fcm.googleapis.com/fcm/send', headers=Headers,
body=json.dumps(Data))

return {'statusCode': 200, 'body': json.dumps('Hello from Lambda!')}

 


<notification으로 구현했을 때>


def lambda_handler(token, context):
FCMToken = token

Data = {'notification': {'title': 'Lotto 보내 드립니다.', 'body': context}, 'to': FCMToken}
Headers = {'Content-type': 'application/json',
'Authorization': 'Key=AAAAR_0..........................uTasJ-DGfJKZkS-ccyNr0xhRHTepcuk4GaFoNMTADl4jvNFM1HYIRqzSLs219BxVA-T9frSd3VCSUIRXXn1PSxhOKgqroBVqTaxmWk'}
http = urllib3.PoolManager().request('POST', 'https://fcm.googleapis.com/fcm/send', headers=Headers,
body=json.dumps(Data))

return {'statusCode': 200, 'body': json.dumps('Hello from Lambda!')}

같은 코드 이기는 하지만, 전송하는 parameter에 따라서 그걸 수신 앱에서는 다른 처리를 할 수 있다는 것이 된다. 


그래서 앞으로는 필요에 따라서 parameter을 다르게 해서 전송할 생각이다. 


 


 



notification 으로 호출할 때


 



body 로 호출 할 때


 


두 영상의 미묘한 차이를 찾을 수 있을까???   위에 기술한 fcmReceiceService 코드를 같이 보면서 이해를 해 보면 작은 차이을 알 수 있지 않을까 싶다.


 


오늘도 즐 코딩 ...





오늘의 이야기

https://play.google.com/store/apps/details?id=com.billcoreatech.getLotto



 


Get Lotto 로또 번호를 드립니다. - Google Play 앱


매주 요청한 요일에 로또 번호를 무작위로 보내 드립니다.


play.google.com





앱을 하나 또 게시했어요. 기능 구현을 위해서 firebase을 사용하고
번호 전달을 위해 python을 이용했습니다.  random 번호를 추출하기 때문에 당첨이 되거나 하지는 않겠지만, 
python을 이용해서 firebase readtime database을 활용하는 방법이 나올 듯합니다. 


 


또한 24시간 365일 컴터 하나를 켜 두어야 하는데, 그럴 수는 없으니, rasberry pi 3을 사용하기로 했습니다. 


os는 linux 계열을 사용했고요. 


 


crontab에 등록을 해 두었기 때문에 매일 반복적인 작업이 가능합니다. 


앱 설명서




게시된 앱의 설명이에요.


이제 random 을 이용해서 숫자를 만들고 전송하는 python code을 살펴볼게요...


import datetime
import json
import random
import sqlite3

# 2021.11.13 FCM 전송을 위한 수정
import firebase_admin
import requests
import telegram
import urllib3
from bs4 import BeautifulSoup
from firebase_admin import credentials
from firebase_admin import db

# 로컬에 저장을 해 두기 위해서 python 코드로 sqlite 테이블 만들어서 사용
conn = sqlite3.connect("opDeGangDB.sqlite", isolation_level=None)
# 커서 획득
rs = conn.cursor()
# 테이블 생성 (데이터 타입은 TEST, NUMERIC, INTEGER, REAL, BLOB 등)
rs.execute(''' CREATE TABLE IF NOT EXISTS LottoResult
(id integer PRIMARY KEY,
hei text,
num1 text,
num2 text,
num3 text,
num4 text,
num5 text,
num6 text,
bonus text)
''')

# 저장된 마지막 회차를 구하기 위해서
# 저장된 값이 string 이라서 int 타입으로 변환해서 찾음.
rs.execute('''
select max(cast(hei as integer)) from LottoResult
''')
getHei = rs.fetchone()[0]

print(getHei)

# 마지막 회차는 삭제
# 아래에서 다시 받음.
rs.execute('''
delete from LottoResult
where hei = {0}
'''.format(getHei))

# 동행복권 사이트에서 당첨 숫자를 파싱 하기 위해서
def getNumParase(getNum):
# print(getNum)
x = getNum.split('+')
bonus = x[1].replace('.','')
num = x[0].split(',')
num.append(bonus)
return num

# 동행복권 사이트 읽어오기
# 1등 당첨 번호만 마자막 회차와 다음 회차를 구함
# 그래야 최종 당첨 회차까지 구해짐.
baseUrl = "https://dhlottery.co.kr/gameResult.do?method=byWin"
for id in range(getHei, getHei+2):
parameter = {'drwNo': id}
res = requests.post(baseUrl, params=parameter)
bs = BeautifulSoup(res.text, 'html.parser')
desc = bs.find(id="desc")
getText = desc['content']
getNow = getText.split(' ')[1].replace('회','')
getNum = getNumParase(getText.split(' ')[3])
print(getNow + " " + str(getNum))

# 한번 구해진 다음에는 null 이 오는 경우가 있어서
try:
rs.execute('''
insert into LottoResult (hei, num1, num2, num3, num4, num5, num6, bonus)
values ({0}, {1}, {2}, {3}, {4}, {5}, {6}, {7})
'''.format(getNow, getNum[0], getNum[1], getNum[2], getNum[3], getNum[4], getNum[5], getNum[6]))
except:
pass

token = "2052..........................FBZM71o"
id = "8.....0"

# 텔레그램 봇으로 실행 되고 있음을 알려 주기 위해서...
bot = telegram.Bot(token)
now = datetime.datetime.now()

androidMsg = "이번 주({0}) 로또 번호를 알려 드려요...\n".format(now)
bot.sendMessage(chat_id=id, text=androidMsg)

# randint 을 이용해서 숫자 만들기
def getNum():
number = []
for i in range(0, 6):
iNo = random.randint(1, 45)
try:
number.index(iNo)
except:
number.append(iNo)
return number

# 여기서 5회차 분의 숫자를 구함.
def getLotto645(androidMsg):
for i in range(0, 5):
while True:
numX = getNum()
numX.sort()
if len(numX) == 6:
break
androidMsg += json.dumps(numX) + "\n"

return androidMsg

bot.sendMessage(chat_id=id, text="행복한 한 주 되세요")

#Firebase database 인증 및 앱 초기화
cred = credentials.Certificate('./services_firebase.json')
firebase_admin.initialize_app(cred,{
'databaseURL' : 'https://getlo.....................atabase.app'
})

# FCM 전송을 하기 위해서 구한 함수 medium.com 에서 구함.
def lambda_handler(token, context):
FCMToken = token

Data = {'data': {'title': 'Lotto 보내 드립니다.', 'body': context}, 'to': FCMToken}
Headers = {'Content-type': 'application/json',
'Authorization': 'Key=AAAAR_0M6y...................xVA-T9frSd3VCSUIRXXn1PSxhOKgqroBVqTaxmWk'}
http = urllib3.PoolManager().request('POST', 'https://fcm.googleapis.com/fcm/send', headers=Headers,
body=json.dumps(Data))

return {'statusCode': 200, 'body': json.dumps('Hello from Lambda!')}

# 전송해줄 요일을 구하기 위해서.
days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
weekDay = datetime.datetime.today().weekday()

# firebase 에 저장된 사용자 정보 획득.
userInfo = db.reference('UserInfo')
sendNumber = db.reference('sendNumber')

# print(type(userInfo.get()))
for val in userInfo.get().values():
# 오늘 보내야 하는 것만 보냄.
if val["weeklyReceived"] in days[weekDay]:
androidMsg = ""
# 범호를 구하고
androidMsg = getLotto645(androidMsg)
# 번호를 보내주고
lambda_handler(val['userToken'], androidMsg)
print('send to ' + val['userToken'] + " " + val["weeklyReceived"] + " sendNumber(" + androidMsg + ")")
# 보내준 번호 기록
sendNumber.update({val['accountId'] : {'accountId': val['accountId'], 'sendNumber': androidMsg, 'sendDateTime': '{0}'.format(datetime.datetime.now())}})

# 작업 끝.
print('END JOB ...')

자료 전송을 위해서 SQlite을 이용해서 파싱 된 데이터를 참조해서 로또 당첨 번호를 기록해 두고 있어요.  


 


중간에 lambda_handler 함수는 다른 분이 posting 하신 코드를 참조해서 FCM 전송을 하고 있는데, 설정만 잘하면 전송하는 것은 문제가 없이 잘 사용하고 있습니다. 


https://medium.com/@sunyi233/python%EC%9C%BC%EB%A1%9C-fcm-push-%EB%B3%B4%EB%82%B4%EA%B8%B0-431b7a17c151



 


Python으로 FCM push 보내기


Firebase엔 FCM이라는 Push 서비스가 있습니다. 간단한 설정으로 안드로이드 앱이나 아이폰 앱에 push를 보낼 수 있습니다.


medium.com




 


다음 posting에서는 안드로이드 앱에 관한 부분을 기술해 볼게요.


 


좋은 일이 있기를 바라요.





오늘의 이야기


#billcorea #운동동아리관리앱
🏸スチーニーたち、バドミントン同好会必須アプリ登場!
👉マッチプレイ - スコア記録&試合相手を探す🎉
一人で、友達、同好会どこでもぴったりです! 🤝
バドミントン好きならぜひ使ってみてください

アプリショートカット👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

이 나라에는 여러 나라의 사람 들어와 살고 있다.  코리안 드림을 꿈꾸며... 한 때는 이 나라의 사람들이 아메리칸드림을 꿈꾸며, 미국으로 떠났던 것처럼... 그래서 이번에는 다국적 언어를 이용한 채팅앱을 하나 만들어 볼까 한다.   잘 될지는 모르겠...