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

무언가를 만들어 내기는 했지만
알림을 만드는 재주는 없으니 ...
답답하네요.
이런걸 필요로 하는 사람이 있는 지도 모르겠고...
어떻게 하면 좋을까요?
2025.02.11 이 앱의 새 얼굴 링크 입니다.
* 이 앱의 사용은 페이앱 (https://payapp.kr)에서 제공하는 API을 지원합니다.
** 이 앱은 페이앱 리셀러 활동을 위해서 제작 배포 됩니다.
*** 이 앱에서 페이앱 회원 가입을 하는 경우에만 정산 수수료의 3%(부가세별도)의 지정이 가능 합니다.
이 앱에서는 이메일 회원 가입 후 로그인 및 구글 계정, 카카오톡 ID, 네이버 ID을 통한 간편 로그인도 지원됩니다.

** 테스트 계정 안내
이 앱의 테스트 운영을 위해서 테스트 계정을 제공합니다.
프로필관리에서는 간편 로그인의 경우는 해당 사이트에서 등록된 프로필 사진, 별명, 이메일 주소가 자동을 설정됩니다.
이메일 로그인의 경우만 프로필 사진을 임의로 등록할 수 있도록 지원합니다.
** 사진 등록을 위해서는 앱 처음 실행 시에는 카메라 권한 허가를 요청하게 되어 있습니다.
이 화면에서 보이는 아이콘의 기능은 다음과 같습니다.

이 화면에 표시되는 아이콘의 기능은 다음과 같습니다.
또한 각 목록에 달려 있는 아이콘은 다음과 같습니다.

여기까지 표시된 정보를 순서에 따라 이미지를 하나로 모아 보았습니다. 간편 로그인의 경우는 해당 사이트에서 제공하는 동의 화면에서 동의 여부를 선택해 주어야 합니다.

입력된 자료를 엑셀로 받아 볼 수 있도록 지원하고 있습니다. 다만, 현재 버전에서는 xls (2007 이전 버전)으로만 지원합니다. 보고서 생성 버튼을 클릭하게 되면 생성된 정보가 파일 공유하는 방식으로 필요한 곳으로 보낼 수 있습니다. 공유 방법은 사용자의 선택에 따라 달라집니다.
이메일로 보내기, 카톡으로 보내기, 문자로 보내기 등등

아래 링크의 동영상 정보를 참고해 보시면 됩니다. 예제 영상은 QR코드를 인식한 이후 결제를 진행하는 영상이나,
이 앱에서는 결제 요청을 진행하는 부분부터 시작됩니다.
https://www.instagram.com/reel/Cm_OTWTjbW_/?utm_source=ig_web_copy_link
Instagram의 BillcoreaTech님 : "#우연히사장 #간편결제 #페이앱 #payapp #billcorea #주문이요
😍 간편결제
1 Likes, 0 Comments - BillcoreaTech (@billcoreatech) on Instagram: "#우연히사장 #간편결제 #페이앱 #payapp #billcorea #주문이요 😍 간편결제는 어떻게 처리 되나요?"
www.instagram.com
help@billcorea.com으로 메일을 주시면 필요한 업무에 대한 지원 해 드립니다.
https://play.google.com/store/apps/details?id=com.billcoreatech.remotepayment0119
리모트 페이 (결제도 공유가 되나요, 원격 결제) - Google Play 앱
매장에 오시지 않는 고객, 결제를 공유해 보세요.
play.google.com
한번 사용해 보시고 다양한 의견 주시면 감사 하겠습니다.
https://billcorea.tistory.com/215
안드로이드 앱 만들기 : 주소 API 사용해 Kakao 우편번호 서비스 활용해 보기
앱을 만들다 보니, 주소 검색을 해야 하는 경우가 생긴다. 구글에서 찾아보면 추천해주는 방법이 2가지 정도로 압축 된다고 볼 수 있을 것 같다. 1. Daum 우편번호 서비스 장정 : API 가 필요하지 않
billcorea.tistory.com
오늘은 이전 포스팅의 내용에서 일부 변형된 모습에 대한 이야기를 잠시해 보겠습니다.
이전 포스팅에서는 webView을 layout.xml을 이용해서 구현한 모습에 대한 이야기를 했습니다.
오늘 구현한 소스는 jetpack compose 을 이용해서 구현한 코드입니다.
먼저 gradle 파일에 webview 사용을 위해서 선언을 해야 합니다. android API 33을 target으로 하고 있어서 아래 버전으로 설정을 해 주면 가능합니다.
// Webview
implementation "com.google.accompanist:accompanist-webview:0.24.13-rc"
webview을 호출하는 activity는 다음과 같이 전체 코드를 작성했습니다. 주의해서 봐야 할 부분은 다음과 같습니다.
이런 정도의 역할들에 문제가 없도록 구현하는 것이 체크가 되어야 할 부분으로 생각이 됩니다.
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.webkit.JavascriptInterface
import android.webkit.WebView
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.billcoreatech.remotepayment0119.ui.theme.RemotePayment0119Theme
import com.google.accompanist.web.*
class AddressFindActivity : ComponentActivity() {
inner class MyJavaScriptInterface {
@JavascriptInterface
fun processDATA(data: String?) {
Log.e("TAG", "error ...........................${data}")
val intent = Intent()
intent.putExtra("data", data)
setResult(AppCompatActivity.RESULT_OK, intent)
finish()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
RemotePayment0119Theme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
WebViewForAddress(this@AddressFindActivity,
doFinish = {
val intent = Intent()
setResult(AppCompatActivity.RESULT_CANCELED, intent)
finish()
}
)
}
}
}
}
}
@SuppressLint("JavascriptInterface")
@Composable
fun WebViewForAddress(addressFindActivity: AddressFindActivity, doFinish:() -> Unit) {
val blogspot = "https://billcoreatech.blogspot.com/2022/06/blog-post.html"
val webViewState =
rememberWebViewState(
url = blogspot,
additionalHttpHeaders = emptyMap()
)
val webChromeClient = AccompanistWebChromeClient()
val webViewNavigator = rememberWebViewNavigator()
WebView(
state = webViewState,
client = object : AccompanistWebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
view?.loadUrl("javascript:sample2_execDaumPostcode();")
}
},
chromeClient = webChromeClient,
navigator = webViewNavigator,
onCreated = { webView ->
with(webView) {
settings.run {
javaScriptEnabled = true
domStorageEnabled = true
javaScriptCanOpenWindowsAutomatically = true
}
addJavascriptInterface(addressFindActivity.MyJavaScriptInterface(), "Android")
}
}
)
BackHandler(enabled = true) {
if (webViewNavigator.canGoBack) {
webViewNavigator.navigateBack()
} else {
doFinish()
}
}
}코드 중에 있는 blogspot의 URL 은 제가 만들어둔 blog의 URL 페이지입니다. 서버가 없기 때문에 blog 을 이용해서 사용하는 페이지 입니다. 실제 처리는 카카오 API에서 하는 것이기 때문에 특별한 문제는 없을 듯합니다.

이렇게 해서 실행해 보면 카카오 API에서 선택한 주소 정보를 받아와서 작업 중인 입력창에 값이 채워지는 것을 확인할 수 있었습니다.
주소 검색창을 작성하시는 데 도움이 되기 바랍니다.
지난번 이야기는 미리 보고 오시면 도움이 됩니다.
https://billcorea.tistory.com/308
안드로이드 앱 만들기 : 소셜 로그인 ( 네이버, 카카오톡, 구글) 구현해 보기
소셜로그인 요새는 대부분의 사용자들이 이런저런 SNS 등에 가입이 되어 있기 때문에 또 다른 개인정보를 제공해 가면서 로그인을 하려고 하지 않습니다. 또한 각각에 등록된 비밀번호를 기억하
billcorea.tistory.com
이번에는 Facebook으로 firebase에 로그인하는 과정을 만들어 보겠습니다.
이미지 버튼 아이콘을 만들기 위한 svg 이미지 입니다. 다른 방법도 있기는 하겠지만, 저는 이미지 버튼으로 구현을 할 생각이기 때문에 간단한 이미지를 만들어 보았습니다.
facebook 로그인을 하려면 해야할 일이 먼저 facebook 개발자 계정을 만들어야 하고 해당 계정에 필요한 앱을 등록해야 합니다. 아래 그림과 같이 facebook 개발자 페이지에서 등록한 앱의 정보와 firebase 인증 설정에서 연결해야 하는 부분을 기술해 보았으니 참고해 보세요.

설정 - 기본설정 에서 앱 ID 와 앱 시크릿코드 가 있어야 firebase 인증 정보에 등록을 할 수 있습니다.
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 계정을 통한 로그인이 해 볼 수 있겠습니다.
전체 소스는 아래 github 에서 참고 하세요.
https://github.com/nari4169/RemotePayment0119

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

동백은 겨울에 피는 꽃이라 했는 데, 그 꽃이 화려하게 피어나는 것을 봐서는 이제 겨울도 곁을 떠나 가려 합니다. .이렇게 시간이 흐르고 나면, 다시금 우리에게는 어느덧 봄이 와 있을 거라 믿습니다. 그렇게 봄이 와야 다시금 여름이 올 테니까요.
계절의 변화가 뚜렷하게 와 닿는 것만으로도 축복이라고 생각합니다. 아직 살아 있다는 증거 이기도 하기 때문입니다. 간혹은 신문기사를 통해 다가오는 뉴스들에서 왜 그런 생각을 해야 했는지, 왜 그렇게 해야 했는지 하는 의문이 들기도 합니다.
결국 누구나 왔다가 가는 것은 다 같은 것인데 말입니다. 그래도 아직 살아 있으니 꿋꿋하게 살아내야 겠지요. 이 보다도 더 힘들었던 시기를 흔적도 없이 다녀가 수많은 무명들처럼 말입니다. 그들에게도 이름이라는 것이 있기는 했을 테니 무명은 아니겠네요.

다시금 아침이 되었는 데도 아직 이런 울울한 느낌의 글을 적어보고 있습니다. 새날은 새 태양이 떠 오를텐데 말입니다.
오늘의 아침은 흐릿한 날씨 때문인지 기분이 그러 맑음은 아닙니다.

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