2026/03/15

오늘의 이야기

소셜로그인


지난번 이야기는 미리 보고 오시면 도움이 됩니다.


 


https://billcorea.tistory.com/308



 


안드로이드 앱 만들기 : 소셜 로그인 ( 네이버, 카카오톡, 구글) 구현해 보기


소셜로그인 요새는 대부분의 사용자들이 이런저런 SNS 등에 가입이 되어 있기 때문에 또 다른 개인정보를 제공해 가면서 로그인을 하려고 하지 않습니다. 또한 각각에 등록된 비밀번호를 기억하


billcorea.tistory.com




 


Facebook Login


이번에는 Facebook으로 firebase에 로그인하는 과정을 만들어 보겠습니다. 




facebook_03.svg

0.01MB




이미지 버튼 아이콘을 만들기 위한 svg 이미지 입니다.  다른 방법도 있기는 하겠지만, 저는 이미지 버튼으로 구현을 할 생각이기 때문에 간단한 이미지를 만들어 보았습니다. 


 


facebook 로그인을 하려면 해야할 일이 먼저 facebook 개발자 계정을 만들어야 하고 해당 계정에 필요한 앱을 등록해야 합니다.   아래 그림과 같이 facebook 개발자 페이지에서 등록한 앱의 정보와 firebase 인증 설정에서 연결해야 하는 부분을 기술해 보았으니 참고해 보세요.


 


개발자 페이지 연결정보



Facebook 개발자페이지 앱정보


설정 - 기본설정 에서 앱 ID 와 앱 시크릿코드 가 있어야 firebase 인증 정보에 등록을 할 수 있습니다. 


 


Firebase Authentication 정보


sign-in method 에서 facebook을 선택하고 등록하는 창을 열어서 OAuth 리다이렉션 URI을 복사해서 facebook 개발자 페이지의 Facebook 로그인 설정의 유효한 OAuth 리다이렉션 URI에 붙여 넣기를 해 주어야 합니다.


 


여기까지가 해야 개발자 페이지 및 인증 설정이 마무리 됩니다.


 


이제 만들던 android 앱을 구현해 보겠습니다. 


 


gradle 파일에는 아래와 같이 추가되어 있어야 합니다.


    // firebase 연동 처리용
implementation platform('com.google.firebase:firebase-bom:31.1.1')
implementation 'com.google.firebase:firebase-auth-ktx:21.1.0'

//facebook login
implementation 'com.facebook.android:facebook-login:15.0.0'

 


다음은 manifest 파일을 살펴보겠습니다.   facebook 개발자 페이지의 가이드에 따라 아래와 같이 구성이 되어야 합니다.


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
// facebook
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove"/>

// facebook
<queries>
<provider android:authorities="com.facebook.katana.provider.PlatformProvider" />
</queries>

<application
android:name=".viewModel.MyApplication"
...
tools:targetApi="31">

// facebook
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
<meta-data android:name="com.facebook.sdk.ClientToken" android:value="@string/facebook_client_token"/>


<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.RemotePayment0119">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

// facebook
<activity android:name="com.facebook.FacebookActivity"
android:configChanges=
"keyboard|keyboardHidden|screenLayout|screenSize|orientation" />
<activity
android:name="com.facebook.CustomTabActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/fb_login_protocol_scheme" />
</intent-filter>
</activity>

</application>

</manifest>

 


이제 acitivity의 구현을 따라가 보겠습니다. 



import android.app.Activity
...

class MainActivity : ComponentActivity() {

...

private val TAG = "---"

// facebook
val callbackManager = CallbackManager.Factory.create()
val loginManager = LoginManager.getInstance()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

...

setContent {

val scrollableState = rememberScrollState()

RemotePayment0119Theme {
// A surface container using the 'background' color from the theme
Column(
modifier = Modifier
.fillMaxSize()
.padding(30.dp)
.verticalScroll(scrollableState),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
DestinationsNavHost(navGraph = NavGraphs.root) {
...
composable(LoginOptionsDestination) {
LoginOptions(
...
doFacebookLogin = {
doFacebookLogin()
},
...
)
}
}
}
}
}
}

private fun doFacebookLogin() {
loginManager.logIn(this@MainActivity, callbackManager, listOf("email", "public_profile","openid"))
loginManager.registerCallback(callbackManager, object : FacebookCallback<LoginResult> {
override fun onCancel() {
Log.e("", "onCancel")
loginManager.logOut()
}

override fun onError(error: FacebookException) {
Log.e("", "error=${error.localizedMessage}")
}

override fun onSuccess(result: LoginResult) {
Log.e("", "accessToken Removed authToken=${result.authenticationToken}")
handleFacebookAccessToken(result.accessToken)
}
})

}

private fun handleFacebookAccessToken(token: AccessToken) {
Log.e(TAG, "handleFacebookAccessToken:$token")

val credential = FacebookAuthProvider.getCredential(token.token)
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
// updateUI(user)
} else {
// If sign in fails, display a message to the user.
Log.e(TAG, "signInWithCredential:failure", task.exception)
Toast.makeText(baseContext, "Authentication failed.",
Toast.LENGTH_SHORT).show()
// updateUI(null)
}
}
}


...

fun printHashKey(context: Context): String {

val TAG = "HASH_KEY"
var hashKey : String? = null
try {
val info : PackageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
for (signature in info.signatures) {
var md : MessageDigest = MessageDigest.getInstance("SHA")
md.update(signature.toByteArray())
hashKey = String(Base64.encode(md.digest(), 0))
Log.e(TAG, "hashKey=$hashKey")
}

} catch (e:Exception){
Log.e(TAG, e.toString())
}

return "$hashKey"

}
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
RemotePayment0119Theme {
DestinationsNavHost(navGraph = NavGraphs.root)
}
}

callBackmanager을 선언한 다음,  loginManager에 callback을 등록해 줍니다. 다음은 loginManager에서 callback 이 돌아왔을 때 처리할 부분을 등록해 주면 됩니다. 


 


handleFacebookAccessToken 함수는 firebase 개발자 가이드에서 설명하고 있는 부분입니다. 저 함수를 호출하는 것으로 firebase에 로그인은 바로 처리됩니다. 이미 등록된 이메일이라면 로그인이 되고 그렇지 않은 경우는 자동으로 사용자 등록을 해 줍니다. 


 


주의할 점



  • facebook에서 돌아오는 accessToken 은 logger cat에 표시되지 않습니다. 로그를 찍어 보면 AccessToken Removed라고 표시되기 때문에 초심자의 경우 헤맬 수 있습니다. 

  • handleFacebookAccessToken 함수에서는 facebook 계정의 이메일을 이용하기 때문에 다른 방법으로 이미 동일한 이메일이 등록되어 있다면 사용할 수 없습니다. 구글 로그인이나, 이메일 로그인을 통해서 이미 가입된 이메일 여부를 확인해 주어야 합니다. 

  • 인증에 대한 부분만 기술하였기 때문에 회원 등록등을 하기 위해서는 정보 수집에 관한 구현은 따로 진행되어야 합니다. 

  • 기능 사용을 위해서는. Facebook 개발자 계정에서 해당 앱에 대한 인증을 진행해야 합니다.


 


이 두 가지만 주의한다면 허송세월하는 일 없이 구현을 해 볼 수 있을 것 같습니다.  카카오나 네이버를 통한 로그인 계정 구현처럼 서버 리스를 구현하지 않아도 되니 구글 로그인 보다 더 수월하게 해 볼 수 있습니다. 


 


 


 



로그인 예시


 


이제 Facebook 계정을 통한 로그인이 해 볼 수 있겠습니다.


 


 


전체 소스는 아래 github 에서 참고 하세요.


https://github.com/nari4169/RemotePayment0119


 





오늘의 이야기

새순



따스해지는 기온을 느끼며, 이제는 봄날이 올 거라는 믿음이 조금은 강하게 느껴져 옵니다.  입춘도 지났고 경칩까지는 아직 보름도 넘게 남아 있기는 하나,  그제보다는 어제가, 어제 보다는 오늘이 조금은 더 따스하게 느껴져 옵니다.


동백



 


동백은 겨울에 피는 꽃이라 했는 데, 그 꽃이 화려하게 피어나는 것을 봐서는 이제 겨울도 곁을 떠나 가려 합니다.   .이렇게 시간이 흐르고 나면, 다시금 우리에게는 어느덧 봄이 와 있을 거라 믿습니다.  그렇게 봄이 와야 다시금 여름이 올 테니까요. 


 


계절의 변화가 뚜렷하게 와 닿는 것만으로도 축복이라고 생각합니다.  아직 살아 있다는 증거 이기도 하기 때문입니다. 간혹은 신문기사를 통해 다가오는 뉴스들에서 왜 그런 생각을 해야 했는지, 왜 그렇게 해야 했는지 하는 의문이 들기도 합니다. 


 


결국 누구나 왔다가 가는 것은 다 같은 것인데 말입니다. 그래도 아직 살아 있으니 꿋꿋하게 살아내야 겠지요. 이 보다도 더 힘들었던 시기를 흔적도 없이 다녀가 수많은 무명들처럼 말입니다.  그들에게도 이름이라는 것이 있기는 했을 테니 무명은 아니겠네요.


 


카랑코에



 


다시금 아침이 되었는 데도 아직 이런 울울한 느낌의 글을 적어보고 있습니다.  새날은 새 태양이 떠 오를텐데 말입니다.


오늘의 아침은 흐릿한 날씨 때문인지 기분이 그러 맑음은 아닙니다. 


잘지내



 


지난 번에 들렸던 카메리아힐에서 보았던 문구입니다.  잘 지내?  벌써 한 달이 지나간 시점인데, 다시금 묻고 싶습니다. 


 


잘 지내?





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle แอปที่สโมสรแบดมินตันต้องมี!
👉 แมทช์เพลย์ – บันทึกคะแนนและค้นหาคู่ต่อสู้ 🎉
เหมาะสำหรับทุกที่ คนเดียว กับเพื่อนฝูง หรือในคลับ! 🤝
ถ้าคุณชอบแบดมินตันลองดูแน่นอน

ไปที่แอป 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

GIF 


그래픽 인터체인지 포맷(영어: Graphics Interchange Format; GIF)은 비트맵 그래픽 파일 포맷이다. 1987년 컴퓨서브가 발표하였으며, 월드 와이드 웹에서 가장 널리 쓰이는 파일 포맷이기도 하다. 특별한 플러그인을 요구하지 않고 여러 환경에서 쉽게 쓸 수 있는 까닭에 다중 프레임 애니메이션을 이용한 배너 광고 등에 널리 쓰인다.


 


wiki 백과에서 옮겨온 글입니다.  다른 설명은 별 의미가 없고 간단한 움짤 만들기를 할 때 도움이 될 것 같습니다.


from PIL import Image
import imageio
import os
save_dir = "C:/Users/nari4/Pictures/gif_source"

path = [f"C:/Users/nari4/Pictures/gif_source/{i}" for i in os.listdir("C:/Users/nari4/Pictures/gif_source")]
imgs = [ Image.open(i) for i in path]
imageio.mimsave('./boss0426_result.gif', imgs, fps=2.0)

 


앱을 만들고 있는 입장에서는 간단한 동작 설명을 하기 위해서 만들어 보면 좋을 것 같습니다.  


 


그래서 하나 만들어 보았습니다. 


 


만들어진 gif



 


우연히 사장이라는 앱의 동작하는 모습을 순서대로 정렬된 이미지 파일을 하나의 폴더에 담아 두고 위에서 작성한 스크립트를 실행하면... 간단하게 움짤(?)이 생성됩니다. 


 


이제 부터는 쉽게 설명서 작업을 해 볼 수 있을 것 같습니다.





오늘의 이야기

 


저멀리 보이는 한라산



지난 설 연휴에 다녀온 제주의 풍경 중 하나입니다 


 


겨울 바다가 시퍼렇다 못해 푸르스름한 것과 구름 사이로 보이는 백록담의 모습, 


 


산방산과 형제섬이 한눈에 들어옵니다.


 


다시 또 가 보려면 언제일까 하는 생각이 들기는 합니다만, 


 


다시금 가 보게 되면 또 다른 모습을 하고 있을 꺼라 생각됩니다. 


 


송악산 일제의 만행



송악산 자락에 저런 굴이 있다는 이야기는 여러 번 들었던 것이기는 하나, 실제로 가 본 것은 이번이 처음인 듯합니다.  여태 살면서 못 보고 살았는지 모르겠지만, 


 


아무튼 우리의 자연도 일제 치하에서 고단한 시간을 지내고 왔다는 생각이 듭니다.  저 자연도 현재의 것이 아니라, 미래에서 잠시 빌려온 것일 텐데 말입니다. 


 


가끔은 이 나라의 선조였던 이들이 조금은 더 힘이 센 나라였다면 어땠을까 하는 생각이 듭니다. 지금과는 다른 모습으로 볼 수 있지 않을까 하는 생각에서 말입니다. 


 


자연 그대로의 자연이 그대로 유지되기를 바랍니다.





오늘의 이야기


#스하리1000명프로젝트,
迷失在韓國?即使您不會說韓語,這個應用程式也可以幫助您輕鬆出行。
只需說出您的語言即可 - 它會翻譯、搜尋並以您的語言顯示結果。
非常適合旅行者!支援英語、日語、中文、越南語等10多種語言。
現在就試試吧!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/03/14

오늘의 이야기

소셜 로그인


firebase에서 지원하고 있는 소셜 로그인(?)은 Google, Facebook, Apple, Microsoft, Twitter 등 대부분 외국계(?)입니다.


firebase 의 소셜 로그인 지원



우리나라에서 대다수가 사용하는 naver, kakao 등은 없습니다. 그래서 지난 이야기에서 Naver, Kakao와 Google 계정으로 로그인하는 이야기를 해 보았습니다.


 


https://billcorea.tistory.com/308



 


안드로이드 앱 만들기 : 소셜 로그인 ( 네이버, 카카오톡, 구글) 구현해 보기


소셜로그인 요새는 대부분의 사용자들이 이런저런 SNS 등에 가입이 되어 있기 때문에 또 다른 개인정보를 제공해 가면서 로그인을 하려고 하지 않습니다. 또한 각각에 등록된 비밀번호를 기억하


billcorea.tistory.com




 


소셜 로그인 은 개인정보가 털린다?


이전 포스팅을 하고 나서 다른 글을 보다가 그런 이야기들이 많이 있더라고요. 소셜 로그인은 쉽게 접근이 가능하기 때문에 여기저기 등록하게 되면 개인정보가 어디서 털리는지 모르게 털린다는 이야기 많습니다. 


 


그 이야기도 이해가 안 되는 부분은 아닙니다. 가입이라는 과정이 없다 보니 쉽게 남발(?) 하고 있지 않는 건가 하는 생각이 드는 건 동의가 되는 부분입니다.


 


카카오의 개인정보 동의 항목



 


네이버 개인정보 동의 항목



 


카카오나 네이버에서는 API을 제공하면서 제공이 되는 범위를 지정하게 되어 있습니다.  해서 개발자 또는 해당 사이트의 개발 범위에서 기본 정보만 확인하고 있다고 한다면, 개인정보 누출에 대해서는 고민이 필요 없지 않을까 하는 생각이 듭니다. 


 


다만, 대부분의 사이트에서는 회원 관리를 위해서 다른 정보를 따로 수집하게 됩니다. 그때는 이제 카카오나 네이버의 고객 정보가 아닌 해당 사이트 또는 앱의 고객정보가 되기 때문에 다른 이야기가 됩니다. 


 


고객정보가 털린다 에는 일부는 맞고 일부는 틀린 이야기가 되는 것 갔습니다.  이제 다시 하던 이야기로 돌아와서...


 


Firebase Auth


https://firebase.google.com/docs/auth/android/start?hl=ko 



 


Android에서 Firebase 인증 시작하기


Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기 이 페이지는 Cloud Translation API를 통해 번역되었습니


firebase.google.com




개발자 문서를 읽어 보면 여러 가지 방법이 기술 되어 있습니다. 이중에서 기본은 email, password 을 받아서 처리 하는 방식이고, 그외 위에서 말했던 바와 같은 google, facebook, twitter 등등 여러가지 로그인을 지원합니다. 


 


다만, 회원 정보의 관리등은 따로 구현을 해야 하는 부분이 있다는 것은 이해를 하셨을 거라고 생각됩니다. 


 


. signinWithCustomToken


개발된 토큰을 이용해 로그인이라고 이해를 했습니다. 그래서 카카오나 네이버에서 로그인 시도 후에 얻어지는 accessToken 만 있으면 로그인이 되지 않을까 하는 기대감(?)으로 찾아보던 중에 그 이해가 잘못되었다는 것을 알아내는 데 까지 일주일은 가버린 느낌입니다.  사이에 설 연휴가 있었기는 했지만요.


 


https://firebase.google.com/docs/auth/android/custom-auth?hl=ko 



 


Android에서 커스텀 인증 시스템을 사용하여 Firebase에 인증  |  Firebase 인증


Firebase Summit에서 발표된 모든 내용을 살펴보고 Firebase로 앱을 빠르게 개발하고 안심하고 앱을 실행하는 방법을 알아보세요. 자세히 알아보기 의견 보내기 Android에서 커스텀 인증 시스템을 사용


firebase.google.com




개발자 문서에서 말하는 이야기 중에서 


 


3. 사용자가 앱에 로그인하면 사용자의 로그인 인증 정보(예: 사용자 이름과 비밀번호)를 인증 서버로 전송하세요. 서버가 사용자 인증 정보를 확인하여 정보가 유효하면 커스텀 토큰을 반환합니다.


 


이 부분에 있는 인증 서버가 발목을 잡을 거라는 걸 알아내는 데 일주일이 가버린 것입니다. 


 


인증서버


그래서 이제 인증 서버에 대한 사례들을 찾아보았습니다.  


https://blog.naver.com/PostView.naver?blogId=chltmddus23&logNo=221784299552 



 


custom auth token을 활용한 firebase auth- kakao 로그인 연동 방법


Sign In Firebase with additional auth providers (ex. kakao ) using Custom auth tokens Use Kaka...


blog.naver.com




이 글에서 보는 바와 같이 인증 서버에서 하는 일이 다음과 같이 있어야 한다는 것을 알게 되었습니다.  custom token을 만들어 내려면 말이죠.


 


custom flow



 


이제 그 구현을 해 보아야 하는 데, 찾을 수 있던 예제등은 node.js로 되어 있는 코드들 이더라고요.  앞전 포스팅에서 이야기했던 것처럼 python으로 구현했던 server less 말고는 아는 게 없는 데 말입니다.  그래서 이제 그 구현을 python 으로 해 보겠습니다. 


 


 


Google Cloud Functions 코드 만들어 보기


서버가 없는 개발자는 이렇게 구현을 해 보았어요. google cloud functions을 만들어 사용해 보니 테스트 설정등에 노력이 들어가기는 하지만,  유용하게 사용을 해 볼 수 있다는 점은 좋은 것 같습니다.  물론 자잘한 비용이 발생됩니다.  구글도 그냥 free로 해 주지는 않으니까요.


from flask import escape
import functions_framework

import json

import firebase_admin
from firebase_admin import credentials
from firebase_admin import db
from firebase_admin import auth
import requests

cred = credentials.Certificate('./boss0426-firebase.json')
firebase_admin.initialize_app(cred, {
'databaseURL': 'https://boss*************************************tabase.app/'
})


# kakao 에서 개인 정보 받아 오기
def requestMe(token):
kakaoRequestMeUrl = 'https://kapi.kakao.com/v2/user/me?secure_resource=true'
header = {'Authorization': 'Bearer ' + token}
resp = requests.get(kakaoRequestMeUrl, headers=header)
return resp.content

# 네이버 에서 개인 정보 받아 오기
def naverRequestMe(token):
naverRequestMeUrl = 'https://openapi.naver.com/v1/nid/me'
header = {'Authorization': 'Bearer ' + token, 'X-Naver-Client-Id': 'oXV*********W', 'X-Naver-Client-Secret': 'Ue*********x'}
resp = requests.get(naverRequestMeUrl, headers=header)
return resp.content


# auth 에 등록된 경우는 update 아니면 create 하는 함수
def updateOrCreateUser(email, nickname, profileImage):
try:
user = auth.get_user_by_email(email)
print('user={0}'.format(user))
user = auth.update_user(uid=user.uid, email=email, email_verified=True, display_name=nickname, photo_url=profileImage)
except:
user = auth.create_user(email=email, email_verified=True, display_name=nickname, photo_url=profileImage)

print('Successfully fetched user data: {0}'.format(user.uid))
return user.uid

# google 로그인 을 위한 함수
@functions_framework.http
def boss0426_google_token(request):
request_json = request.get_json(silent=True)
request_args = request.args

profileImage = 'https://billcorea.tistory.com'
if request_json and 'email' in request_json:
email = request_json['email']
nickName = request_json['nickName']
elif request_args and 'email' in request_args:
email = request_args['email']
nickName = request_args['nickName']
else:
email = 'email'
nickName = 'nickName'

uid = updateOrCreateUser(email, nickName, profileImage)
customToken = auth.create_custom_token(uid)
resp = json.dumps({"customToken": '{}'.format(customToken)})
print(resp)
return resp, 200

# 네이버 에서 access token 을 받아온 경우
@functions_framework.http
def boss0426_naver_token(request):
request_json = request.get_json(silent=True)
request_args = request.args

if request_json and 'accessToken' in request_json:
accessToken = request_json['accessToken']
elif request_args and 'accessToken' in request_args:
accessToken = request_args['accessToken']
else:
accessToken = 'accessToken'

textResp = naverRequestMe(accessToken)
dict = json.loads(textResp)
print('dict={}'.format(dict))
userId = accessToken
email = dict['response']['email']
nickname = dict['response']['nickname']
profileImage = dict['response']['profile_image']

print('userId {}'.format(userId))
print('email {}'.format(email))
print('nickname {}'.format(nickname))
print('profileImage {}'.format(profileImage))

if userId == "":
return "ERROR", 400

uid = updateOrCreateUser(email, nickname, profileImage)
customToken = auth.create_custom_token(uid)
resp = json.dumps({"customToken": '{}'.format(customToken)})
print(resp)
return resp, 200

# 카카오 에서 access token 을 받아온 경우
@functions_framework.http
def boss0426_kakao_token(request):
request_json = request.get_json(silent=True)
request_args = request.args

if request_json and 'accessToken' in request_json:
accessToken = request_json['accessToken']
elif request_args and 'accessToken' in request_args:
accessToken = request_args['accessToken']
else:
accessToken = 'accessToken'

textResp = requestMe(accessToken)
dict = json.loads(textResp)
print('dict={}'.format(dict))
userId = accessToken
email = dict['kakao_account']['email']
nickname = dict['properties']['nickname']
profileImage = dict['properties']['profile_image']

print('userId {}'.format(userId))
print('email {}'.format(email))
print('nickname {}'.format(nickname))
print('profileImage {}'.format(profileImage))

if userId == "":
return "ERROR", 400

uid = updateOrCreateUser(email, nickname, profileImage)
customToken = auth.create_custom_token(uid)
resp = json.dumps({"customToken": '{}'.format(customToken)})
print(resp)
return resp, 200

코드는 위에서 기술했던 flow에 준해서 작성을 했습니다. 



  • 네이버나 카카오의 경우는 access Token을 받아와서 이 코드 안에서 필요한 정보(email, nickname, profile_image)를 취한 다음, auth 정보에 보유 유무를 체크하고 없을 때는 create 하고 있으면 update 하는 방식으로 하고, 

  • google onetap 로그인의 경우는 access token 이 아니라 email 정보를 주기 때문에 그냥 auth 정보를 확인한 뒤 create 나 update을 하는 방식으로 구현이 되었습니다.


카카오나 네이버의 정보 취득은 url을 get 방식으로 호출하면 json type으로 정보를 전달해 주기 때문에 그렇게 사용이 가능합니다.   카카오와 네이버의 다른 점은 호출 시 사용하는 header 기술에 차이가 있습니다.  코드에서 잘 찾아보세요.


 


한 가지 아쉬운 점은 customToken으로 나가는 값을 json으로 만들어 내보는 과정에서 b' 이런 것들이 붙어 간다는 부분인데, 일단 현재는 android code에서 split 처리를 해서 사용을 하고 있기는 합니다.  이 부분은 나중에 알게 되면 다시 수정을 해 보겠습니다.  혹여나 알게 되시면 댓글  부탁 드립니다.


 


이제 코드는 작성이 되었습니다.  Google Cloud Functions 에는 어떻게 적용을 할까요? 그건 이전 포스팅을 참고해 주세요.


 


오늘도 좋은 하루가 되시길 바라 봅니다.


 





오늘의 이야기

소셜로그인


요새는 대부분의 사용자들이 이런저런 SNS 등에 가입이 되어 있기 때문에 또 다른 개인정보를 제공해 가면서 로그인을 하려고 하지 않습니다. 또한 각각에 등록된 비밀번호를 기억하는 것이 이제 한계에 도달하기도 합니다.

그래서 사용 하는 것이 기존에 등록된 SNS계정을 이용해서 로그인하는 것을 선호(?)하는 경향이 있어 보입니다. 그래서 이번에는 우리가 만드는 앱에도 소셜 로그인을 구현해 볼까 합니다.

또한, firebase auth 을 통과하는 것까지 구현 보는 것이 이번 앱을 만들면서 고민했던 부분입니다.


로그인 화면




일반적으로 만들어지는 로그인 화면 예시 입니다. 이 화면에서는 기존과 같이 이메일과 비밀번호를 받아서 로그인하는 방식을 지원합니다. (사전에 등록을 해야 하기 때문에 등록하는 화면도 따로 구현이 되어야 합니다.)

다음은 소셜 로그인을 지원하는 버튼을 그려 보겠습니다. 아이콘은 여기 저기서 이미지는 구해지기는 하나 이걸 또 변환을 해서 사용해야 하기 때문에 android studio의 메뉴에서 File - New - Vector Asset에서 사용할 vector 이미지 소스 파일은 아래 붙여 두도록 하겠습니다.




Google-08.svg

0.00MB





kakao.svg

0.00MB





naver.svg

0.00MB




※ 해당 이미지 들은 구글링을 통해서 얻어진 파일들 입니다. 여기저기서 구현 이미지 파일을 svg로 변환해서 사용했습니다.


Kakao 로그인


사전에 kakao 개발자 계정을 통해서 앱을 등록하고 네이티브 Key는 얻어 왔다고 가정하겠습니다. 그 과정은 이번 이야기의 중심이 아니기 때문입니다.

제가 하는 경우는 settings.gradle 파일에 추가한 부분인데, kakao sdk 등을 내려받을 경로를 지정해 주었습니다.


repositories {
google()
mavenCentral()
gradlePluginPortal()
maven { url 'https://devrepo.kakao.com/nexus/content/groups/public/' }
}


다음은 앱 수준의 gradle 파일에 추가할 부분입니다.
이 글을 쓰고 있는 2023.01.30 기준으로는 2.12.1 이 최신인 듯합니다.


dependencies {

...
// kakao login
implementation "com.kakao.sdk:v2-user:2.12.1"

}


다음은 activity에 기술된 부분입니다. callback을 지정하고 작업하는 부분은 kakao의 경우 대부분의 사용자들이 카카오톡을 사용하고 있으며, 로그인도 하고 있다고 생각이 되지만 혹여나 카카오톡이 로그인되지 않을 경우 카카오 계정으로 로그인을 하기 위해서 구현하였습니다.



  • isKakaoTalkLoginAvailable : 카카오톡 로그인 가능 한가?

  • loginWithKakaoTalk : 카카오톡으로 로그인

  • loginWithKakaoAccount : 카카오 계정으로 로그인


결국 로그인은 카카오톡에 계정이 있어야만 가능하겠지만, 2가지 방법을 기술해 두고 선택적으로 로그인을 할 수 있도록 지원하는 것이 AVD (애물레이터) 등에서 테스트할 때도 도움이 됩니다.


private val kakaoCallback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
if (error != null) {
Log.e(TAG, "카카오계정으로 로그인 실패", error)
} else if (token != null) {
Log.e(TAG, "카카오계정으로 로그인 성공 ${token.accessToken} ${token.idToken}")
doSigninKakaoToken("${token.accessToken}")
}
}

private fun doKakaoLogin() {

// 카카오톡이 설치되어 있으면 카카오톡으로 로그인, 아니면 카카오계정으로 로그인
if (UserApiClient.instance.isKakaoTalkLoginAvailable(this@MainActivity)) {
UserApiClient.instance.loginWithKakaoTalk(this@MainActivity) { token, error ->
if (error != null) {
Log.e(TAG, "카카오톡으로 로그인 실패", error)

// 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
// 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
return@loginWithKakaoTalk
}

// 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인 시도
UserApiClient.instance.loginWithKakaoAccount(this@MainActivity, callback = kakaoCallback)
} else if (token != null) {
Log.e(TAG, "카카오톡으로 로그인 성공 ${token.accessToken} ${token.idToken}")
doSigninKakaoToken("${token.accessToken}")
}
}
} else {
UserApiClient.instance.loginWithKakaoAccount(this@MainActivity, callback = kakaoCallback)
}

}

 


Naver 로그인


이 또한 네이버 개발자 계정으로 로그인을 하고 앱을 생성한 다음 naver login api 사용을 하겠다는 부분과 앱을 등록하는 과정이 들어갑니다. 이 부분은 검색을 통해서 많이 찾을 수 있고, 이번 포스팅의 주제는 아니기 때문에 넘어갑니다.
네이버에서는 등록한 앱의 Client ID와 Client Secret을 얻어와야 합니다.

네이버의 경우는 카카오와 다르게 repogitories을 선언하지 않아도 되었습니다. 네이버 개발자 가이드의 내용에는
oauth-5.3.0.aar 파일을 받아서 libs에 넣고 선언을 하라는 부분이 있기도 하지만, 실제로는 아래와 같이 gradle 파일에 선언을 하는 것으로 해소가 되었습니다.


dependencies {

...
// naver login
implementation("com.navercorp.nid:oauth:5.3.0") // jdk 11

}

다음은 activity에서 구현되어야 하는 부분입니다. naverLauncher을 먼저 선언하고 호출된 결과를 받아올 준비를 합니다.
그리고 아래와 같이 초기화 및 호출을 해 봅니다.



  • initialize : 초기화

  • authenticate : 네이버 로그인을 호출


    private val naverLauncher = registerForActivityResult<Intent, ActivityResult>(ActivityResultContracts.StartActivityForResult()) { result ->
when(result.resultCode) {
RESULT_OK -> {
// 네이버 로그인 인증이 성공했을 때 수행할 코드 추가
// binding.tvAccessToken.text = NaverIdLoginSDK.getAccessToken()
// binding.tvRefreshToken.text = NaverIdLoginSDK.getRefreshToken()
// binding.tvExpires.text = NaverIdLoginSDK.getExpiresAt().toString()
// binding.tvType.text = NaverIdLoginSDK.getTokenType()
// binding.tvState.text = NaverIdLoginSDK.getState().toString()
Log.e("", "accessToken=${NaverIdLoginSDK.getAccessToken()}")
doSigninNaverToken(NaverIdLoginSDK.getAccessToken().toString())
}
RESULT_CANCELED -> {
// 실패 or 에러
val errorCode = NaverIdLoginSDK.getLastErrorCode().code
val errorDescription = NaverIdLoginSDK.getLastErrorDescription()
doToastMakeAppend(R.string.titleFailure, "$errorDescription")
}
}
}

private fun doNaverLogin() {
NaverIdLoginSDK.initialize(this@MainActivity, BuildConfig.NAVER_CLIENT, BuildConfig.NAVER_SECERT, BuildConfig.APPLICATION_ID)
NaverIdLoginSDK.authenticate(this@MainActivity, naverLauncher)
}

 


구글 로그인


구글 oneTap 로그인의 경우도 다음과 같이 구현이 될 듯합니다. 앱을 등록하거나 하는 부분등은 구글 검색을 통해서 찾아보실 수 있을 듯하고, 이번 포스팅의 주된 이야기가 아니라 생략 합니다. 다만 여기서는 web_client_id 가 필요 합니다.
이건 firebase 개발자 콘솔에서 앱을 등록하고 google_service.json 파일을 가져오면 그 안에서 가져올 수 있습니다.

앱 수준의 gradle 파일에는 다음과 같이 추가했습니다.


plugins {
...
id 'com.google.gms.google-services'
...
}

...

dependencies {

...
// 구글 로그인
implementation 'com.google.android.gms:play-services-auth:20.4.1'

}

프로젝트 수준의 gradle 에는 다음과 같이 추가했습니다.


dependencies {
...
classpath 'com.google.gms:google-services:4.3.15'

}

이제 activity에서의 구현을 보도록 하겠습니다.


 private lateinit var oneTapClient: SignInClient

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

...

oneTapClient = Identity.getSignInClient(this@MainActivity)

}

private val intentSenderRequestActivityResultLauncher =
registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) { result ->
try {
if (result.resultCode == Activity.RESULT_OK) {
result.data?.let { intent ->
oneTapClient.getSignInCredentialFromIntent(intent)
}?.also { signInCredential ->

val username = signInCredential.id
val displayName = signInCredential.displayName

Log.e("", "loginOk $username, $displayName")

doSigninGoogleToken(username, displayName.toString())

}
}
} catch (e: ApiException) {
when (e.statusCode) {
CommonStatusCodes.CANCELED -> {
Log.d(TAG, "One-tap dialog was closed.: ${e.message.toString()}")
}
CommonStatusCodes.NETWORK_ERROR -> {
Log.d(TAG, "One-tap encountered a network error.: ${e.message.toString()}")
}
else -> {
Log.d(TAG, "Couldn't get credential from result.: ${e.localizedMessage}")
}
}
}
}

/**
* google oneTap 로그인
*/
private fun doGoogleLogin() {

Log.e(TAG, "google login start ...")

GetSignInIntentRequest.builder()
.setServerClientId(getString(R.string.default_web_client_id))
.build().also { getSignInIntentRequest ->

Identity.getSignInClient(this@MainActivity)
.getSignInIntent(getSignInIntentRequest)
.addOnSuccessListener { pendingIntent ->
IntentSenderRequest.Builder(pendingIntent.intentSender).build()
.also { intentSenderRequest ->
try {
intentSenderRequestActivityResultLauncher.launch(
intentSenderRequest
)
} catch (e: ActivityNotFoundException) {
Log.d(
TAG,
"addOnSuccessListener:Google Sign-in failed:$e "
)
}
}

}
.addOnFailureListener { exception ->
Log.d(TAG, "addOnFailureListener:Google Sign-in failed:$exception ")
}
}
}

 


Firebase Auth


위의 방법 등으로 해서 구글, 네이버, 카카오 로그인을 구현을 해 보았습니다. 이제 여기서 구해지는 accessToken을 이용해서 firebase 의 auth 구현 방법 중에서 custom Token 을 이용해 로그인하는 방법으로 연결해 보겠습니다.


auth.signInWithCustomToken


먼저 보고 와야 하는 포스팅이 하나 있습니다.

https://billcorea.tistory.com/279



 


개발일기 # 번외편2 : 난 서버가 없는데(Serverless) cloud function 사용해 보기


Cloud Function 서버를 보유 하지(Serverless) 않고 서버가 있는 것처럼 업무 구현을 하고 싶습니다. 개발을 하면서 데이터 베이스와 스토리지를 firebase을 활용하고 있다면 cloud function 도 배워서 준비를


billcorea.tistory.com




이 글을 보고 이해를 하셨다고 보고 다음 이야기를 진행해 보겠습니다. 쉬었다 오겠습니다. 다음 글에서 뵈어요.

지나가기 전에,

보다보니 소셜로그인으로 인한 개인정보가 분실에 대한 걱정이 생길 수도 있겠네요.  개발자센터에 앱을 등록 하면서 인증시 받는 정보는 최소화 해야 할 듯 하네요.

현재는 닉네임, 이메일주소, 프로파일이미지 정도만 받고 있는 데... 더 줄여야 할까요???


 


 


전체 소스는 아래 github 에서 참고 하세요.


https://github.com/nari4169/RemotePayment0119


 





오늘의 이야기



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

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

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

그것도 구글 Gemini로다가!

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

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

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


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




오늘의 이야기

Translation


번역을 시도하는 방법에는 여러 가지가 있습니다.  그중에는 비용이 들어가는 방법도 있고, 일부 무료인 방법도 있습니다.



  • 카카오 번역  API : 월 10,000자 까지는 무료 이후 1000자 단위로 18원 발

  • 구글 cloud 번역 API : 월 최대 500,000자 까지는 무료 이후부터는 비용 추가 (기본 옵션 선택 시)

  • 네이버 papago text 번역 : 1,000,000 단위 과금  20,000원 (과금 단위는 글자를 항상 올림)


등의 방법을 찾을 수 있습니다. 


 


오늘 하고 싶은 이야기는 간단한 문구를 그냥 번역해 보는 방법입니다. 


 


문서의 길이가 길고, 중요한 문서라고 한다면 비용이 들여서라도 번역은 정확하게 하는 것이 맞을 것 같습니다.  단지,  앱에서 사용하는 API들이 꼬부랑말(대부분 영어)로 되어 있는 것들이라서 API가 제공하는 오류 메시지를 그대로 보여 주는 것은 사용자 편의를 고려하지 않은 것이라 생각하게 되어 간편 번역을 해 보기로 했습니다. 


 


구글에서 제공하는 ML Kit에 보면 여러 가지가 있지만 오늘은 그중에서 Translation의 사용하는 방법에 대한 예시를 적어 두고자 합니다. 


 


https://developers.google.com/ml-kit/language/translation/android



 


Android에서 ML Kit를 사용하여 텍스트 번역  |  Google Developers


이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English Android에서 ML Kit를 사용하여 텍스트 번역 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.


developers.google.com




 


IOS 개발은 아직 해 보지 않기 때문에... 안드로이드로 개발하는 부분만 말해 볼까 합니다.  이 글은 위에 있는 개발 가이드의 내용을 참조하여 작성하였습니다.


Gradle


앱 수준의 gradle 파일에 한 줄 넣어 주세요.


// ML Kit translate
implementation 'com.google.mlkit:translate:17.0.1'

 


Viewmodel


다음은 가이드에 나와 있는 내용들을 다 정리하기가 너무 힘들어서 가이드의 예시용 sourcecode에서 viewModel 코드 파일을 그냥 복사했습니다.


 


/*
* Copyright 2019 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import android.app.Application
import android.util.LruCache
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.billcoreatech.remotepayment0119.R
import com.google.android.gms.tasks.OnCompleteListener
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.Tasks
import com.google.mlkit.common.model.DownloadConditions
import com.google.mlkit.common.model.RemoteModelManager
import com.google.mlkit.nl.translate.TranslateLanguage
import com.google.mlkit.nl.translate.TranslateRemoteModel
import com.google.mlkit.nl.translate.Translation
import com.google.mlkit.nl.translate.Translator
import com.google.mlkit.nl.translate.TranslatorOptions

import java.util.Locale

/**
* Model class for tracking available models and performing live translations
*/
class TranslateViewModel(application: Application) : AndroidViewModel(application) {

companion object {
// This specifies the number of translators instance we want to keep in our LRU cache.
// Each instance of the translator is built with different options based on the source
// language and the target language, and since we want to be able to manage the number of
// translator instances to keep around, an LRU cache is an easy way to achieve this.
private const val NUM_TRANSLATORS = 3
}

private val modelManager: RemoteModelManager = RemoteModelManager.getInstance()
private val pendingDownloads: HashMap<String, Task<Void>> = hashMapOf()
private val translators =
object : LruCache<TranslatorOptions, Translator>(NUM_TRANSLATORS) {
override fun create(options: TranslatorOptions): Translator {
return Translation.getClient(options)
}
override fun entryRemoved(
evicted: Boolean,
key: TranslatorOptions,
oldValue: Translator,
newValue: Translator?,
) {
oldValue.close()
}
}
val sourceLang = MutableLiveData<Language>()
val targetLang = MutableLiveData<Language>()
val sourceText = MutableLiveData<String>()
val translatedText = MediatorLiveData<ResultOrError>()
val availableModels = MutableLiveData<List<String>>()

// Gets a list of all available translation languages.
val availableLanguages: List<Language> = TranslateLanguage.getAllLanguages().map { Language(it) }

init {
// Create a translation result or error object.
val processTranslation =
OnCompleteListener<String> { task ->
if (task.isSuccessful) {
translatedText.value = ResultOrError(task.result, null)
} else {
translatedText.value = ResultOrError(null, task.exception)
}
// Update the list of downloaded models as more may have been
// automatically downloaded due to requested translation.
fetchDownloadedModels()
}
// Start translation if any of the following change: input text, source lang, target lang.
translatedText.addSource(sourceText) { translate().addOnCompleteListener(processTranslation) }
val languageObserver =
Observer<Language> { translate().addOnCompleteListener(processTranslation) }
translatedText.addSource(sourceLang, languageObserver)
translatedText.addSource(targetLang, languageObserver)

// Update the list of downloaded models.
fetchDownloadedModels()
}

private fun getModel(languageCode: String): TranslateRemoteModel {
return TranslateRemoteModel.Builder(languageCode).build()
}

// Updates the list of downloaded models available for local translation.
private fun fetchDownloadedModels() {
modelManager.getDownloadedModels(TranslateRemoteModel::class.java).addOnSuccessListener {
remoteModels ->
availableModels.value = remoteModels.sortedBy { it.language }.map { it.language }
}
}

// Starts downloading a remote model for local translation.
internal fun downloadLanguage(language: Language) {
val model = getModel(TranslateLanguage.fromLanguageTag(language.code)!!)
var downloadTask: Task<Void>?
if (pendingDownloads.containsKey(language.code)) {
downloadTask = pendingDownloads[language.code]
// found existing task. exiting
if (downloadTask != null && !downloadTask.isCanceled) {
return
}
}
downloadTask =
modelManager.download(model, DownloadConditions.Builder().build()).addOnCompleteListener {
pendingDownloads.remove(language.code)
fetchDownloadedModels()
}
pendingDownloads[language.code] = downloadTask
}

// Returns if a new model download task should be started.
fun requiresModelDownload(
lang: Language,
downloadedModels: List<String?>?,
): Boolean {
return if (downloadedModels == null) {
true
} else !downloadedModels.contains(lang.code) && !pendingDownloads.containsKey(lang.code)
}

// Deletes a locally stored translation model.
internal fun deleteLanguage(language: Language) {
val model = getModel(TranslateLanguage.fromLanguageTag(language.code)!!)
modelManager.deleteDownloadedModel(model).addOnCompleteListener { fetchDownloadedModels() }
pendingDownloads.remove(language.code)
}

fun translate(): Task<String> {
val text = sourceText.value
val source = sourceLang.value
val target = targetLang.value
if (source == null || target == null || text == null || text.isEmpty()) {
return Tasks.forResult("")
}
val sourceLangCode = TranslateLanguage.fromLanguageTag(source.code)!!
val targetLangCode = TranslateLanguage.fromLanguageTag(target.code)!!
val options =
TranslatorOptions.Builder()
.setSourceLanguage(sourceLangCode)
.setTargetLanguage(targetLangCode)
.build()
return translators[options].downloadModelIfNeeded().continueWithTask { task ->
if (task.isSuccessful) {
translators[options].translate(text)
} else {
Tasks.forException<String>(
task.exception
?: Exception(getApplication<Application>().getString(R.string.unknown_error))
)
}
}
}

/** Holds the result of the translation or any error. */
inner class ResultOrError(var result: String?, var error: Exception?)

/**
* Holds the language code (i.e. "en") and the corresponding localized full language name (i.e.
* "English")
*/
class Language(val code: String) : Comparable<Language> {

private val displayName: String
get() = Locale(code).displayName

override fun equals(other: Any?): Boolean {
if (other === this) {
return true
}

if (other !is Language) {
return false
}

val otherLang = other as Language?
return otherLang!!.code == code
}

override fun toString(): String {
return "$code - $displayName"
}

override fun compareTo(other: Language): Int {
return this.displayName.compareTo(other.displayName)
}

override fun hashCode(): Int {
return code.hashCode()
}
}

override fun onCleared() {
super.onCleared()
// Each new instance of a translator needs to be closed appropriately. Here we utilize the
// ViewModel's onCleared() to clear our LruCache and close each Translator instance when
// this ViewModel is no longer used and destroyed.
translators.evictAll()
}
}

 


 


MainActivity


예시에 앱에서는 콤보 박스를 이용해서 source 언어와 번역 후 사용할 target 언어를 선택하도록 하였으나, 만들고 있는 앱에서는 영어(en)를 한국어(ko)로 번역하는 것만 사용할 생각 이기 때문에 다음과 같은 부분들을 MainActivity에 추가해 주었습니다. 


 


import android.app.Activity
.....
import java.security.MessageDigest

class MainActivity : ComponentActivity() {

private val translateView : TranslateViewModel by viewModels()

.....

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// 간단한 문서 번역 ML
translateView.sourceLang.value = TranslateViewModel.Language("en")
translateView.targetLang.value = TranslateViewModel.Language("ko")
if (!sp.getBoolean("isDownloadKR", false)) {
translateView.downloadLanguage(TranslateViewModel.Language("ko"))
}
// 한국어 모델은 download 을 한 번 받아야 해서
translateView.availableModels.observe(
this@MainActivity
) {result ->
for (lang in result) {
Log.e("", "lang=${lang}")
if (lang == "kr") {
... // 다운로드 받은 걸 표시 해 두었다가 앱을 실행 할 때 마다 받는 건 방지 하도록 구현
}
}
}

...

setContent {

val scrollableState = rememberScrollState()

RemotePayment0119Theme {
// A surface container using the 'background' color from the theme
Column(
modifier = Modifier
.fillMaxSize()
.padding(30.dp)
.verticalScroll(scrollableState),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
DestinationsNavHost(navGraph = NavGraphs.root) {
composable(JoinUserScreenDestination) {
JoinUserScreen(
navigator = destinationsNavigator,
doSignup = { emailId, password ->
doSignupUser(emailId, password)
}
)
}
composable(LoginOptionsDestination) {
LoginOptions(
doLogin = { email, password ->
doEmailLogin(email, password)
},
doForGotPassword = { email ->
doForgotPassword(email)
},
doGoogleLogin = {
doGoogleLogin()
},
doFacebookLogin = {
doFacebookLogin()
},
doRegisterUser = {
destinationsNavigator.navigate(JoinUserScreenDestination)
},
doTranslateDownload = {
// 화면에서 버튼 클릭을 통해 일부로 받을 수 있도록 구현 할 수 도 있음.
translateView.downloadLanguage(TranslateViewModel.Language("ko"))
doToastMake(R.string.msgBeginDownload)
},
doEmailLoginWithLink = { emailid ->
doEmailLoginWithLink(emailid)
}
)
}
}
}
}
}
}

private fun doEmailLoginWithLink(emailId: MutableState<String>) {

}

private fun doSignupUser(emailId: MutableState<String>, password: MutableState<String>) {

}

private fun doFacebookLogin() {

}

private fun doGoogleLogin() {

}

private fun doForgotPassword(email: MutableState<String>) {

}

private fun doEmailLogin(email: MutableState<String>, password: MutableState<String>) {

if (email.value == "") {
doToastMake(R.string.EnterEmail)
return
}
if (password.value == "") {
doToastMake(R.string.EnterPassword)
return
}

auth.signInWithEmailAndPassword(email.value, password.value)
.addOnSuccessListener {
doToastMake(R.string.msgSigninCompleted)
}
.addOnFailureListener {
doToastMakeAppend(R.string.msgUserIdOrPasswordError, it.localizedMessage)
}
}

private fun doToastMake(msgResource: Int) {
Toast.makeText(this@MainActivity, getString(msgResource), Toast.LENGTH_SHORT).show()
}

private fun doToastMakeAppend(msgResource: Int, localizedMessage: String?) {

// 에러 메시지가 영어로 오기 때문에 그것을 번역해 본다.
// 번역이 된 이후에 toast 가 나오도록 구현
translateView.sourceText.value = "$localizedMessage"
translateView.translatedText.observe(
this@MainActivity
) { result ->
Log.e("", "result = ${result.error} ${result.result}")
resultString = result.result.toString()

Toast.makeText(this@MainActivity, "${getString(msgResource)}:${resultString}", Toast.LENGTH_SHORT).show()
}

}

}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
RemotePayment0119Theme {
DestinationsNavHost(navGraph = NavGraphs.root)
}
}

코드를 살펴보면 onCreate에서 먼저 사용할 언어를 정했습니다. 영어와 한국어로... 여기서 실수를 했던 부분은 한국어를 kr이라고 했던 건데,  무관심하게 kr 이라고 할 수 있으나, 예제 앱에서도 볼 수 있지만, ko라고 해 주어야 됩니다. (다들 아시는 것이라 생각하고 있습니다.)


 


그리고는 doToastMakeAppend 함수에서 인수로 받은 오류 메시지를 번역해 보여 주는 방식으로 코드를 작성했습니다. 아직 잘 알지 못하는 부분은 번역을 위해 전달한 문장이 2줄인 경우 응답이 2번으로 나누어져 오는 것 같다는 것입니다.  그래서 Toast 가 2번 발생됩니다.  그걸 해소하는 건 이 글을 보시는 분들의 몫으로 남겨 둡니다. 


 


아무튼 이렇게 하면 어렵지 않게 영어 오류 메시지를 한글로 번역해서 보여 줄 수 있습니다. 저처럼 firebase의 기능을 활용하시는 분들께 한 가지 팁이 될지 바랍니다.


 


 


활용예시


이렇게 구현된 앱 샘플입니다.  firebase에 email을 통해 로그인하기 위해서 사용자 가입을 시도해 봅니다.  그때 입력된 password의 길이가 6자리 미만의 경우 오류 처리가 됩니다. 


private fun doSignupUser(emailId: MutableState<String>, password: MutableState<String>) {

if (emailId.value == "") {
doToastMake(R.string.EnterEmail)
return
}
if (password.value == "") {
doToastMake(R.string.EnterPassword)
return
}
auth.createUserWithEmailAndPassword(emailId.value, password.value)
.addOnSuccessListener {
doToastMake(R.string.msgCreateUserCompleted)
}
.addOnFailureListener {
doToastMakeAppend(R.string.msgCreateUserFailure, it.localizedMessage)
}
}

이때 나오는 오류 메시지를 doToastMakeAppend 함수 호출을 통해 번역을 시도해 보는 것입니다. 


 



간편 번역 샘플


 


이상으로 ML Translation에 대해 알아보았습니다.


 


읽어 주셔서 감사합니다.


 


 





오늘의 이야기


#스하리1000명프로젝트

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

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

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

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





오늘의 이야기

오늘도 홍보글 하나 달아요... 이 앱은 미니키오스크를 구현합니다. 휴대장치만 있으면 구현됩니다. 앱은 무료지만 기능은 필요한 건 만들어 드려요.