2026/03/10

오늘의 이야기

지난 포스팅에 연속해서



https://billcorea.tistory.com/267







개발일기 #5 PAYAPP 연동을 위한 준비


이번 개발을 시작하면서 준비해야 할 것 중에 하나가 결제처리를 지원하는 PG(Payment GateWay) 연동 이었습니다. 이전 글에서도 적었던 것처럼 일반적인 PG 연동 API 들은 하나 같이 쇼핑몰 앱을 기반(


billcorea.tistory.com





구동되는 이야기는 지난 포스팅에 적었습니다. 참고하시면 될 것 같고요. 오늘은 그것들을 구현하기 위해 했던 코드 구현에 대해서 이야기해 보겠습니다.


HTTP 통신은


일반적으로 API 통신은 HTTP 을 이용해서 호출하라고 합니다. 이번에 사용했던 PAYAPP의 경우에도 동일합니다. 이런 경우 안드로이드에서는 통신과 관련된 몇 가지 해야 할 부분들이 있습니다.

먼저 manifest 에서의 권한 설정 부분이 필요합니다.


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

인터넷 사용에 대한 권한을 등록합니다. 다음은 통신의 하는 경우 네트워크와 관련해서는


<application
...
android:usesCleartextTraffic="true"
... >

<activity
android:name=".MainActivity"
android:exported="true"

...

안드로이드 가이드 발췌


개발자 가이드에서 알려주고 있는 것과 같이 네트워크 트래픽에 대한 설정을 true로 해 주어야 합니다.

그다음 요구되는 사항은 http connect을 맺거나 하는 경우 비동기 설정을 해 주어야 하는 부분이 있습니다. 그래서 구글링을 해보면 여러 가지 방법들이 설명이 되어 있습니다. thread을 이용하여 하거나 그 외 다른 비동기 통신 방법을 구현하고 설명합니다.

https://ktor.io/







Ktor: Build Asynchronous Servers and Clients in Kotlin


Kotlin Server and Client Framework for microservices, HTTP APIs, and RESTful services


ktor.io




이번에는 Ktor 방식을 이용한 비동기 통신을 구현해 보기로 했습니다.



gradle 파일 설정


gradle 파일에는 아래와 같이 ktor을 사용하기 위해서 implementaion을 설정했습니다. 2022.10.20 쯤에는 2.1.2 버전이 최신 버전이라고 되어 있습니다.


//ktor
implementation "io.ktor:ktor-client-core:2.1.2"
implementation "io.ktor:ktor-client-cio:2.1.2"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'


Helper 구현하기


먼저 통신을 위한 helper 을 구현해 봅니다.


import android.util.Log
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.util.date.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class HttpRequestHelper {

companion object {
val TAG: String = HttpRequestHelper::class.java.name
}

private val client: HttpClient = HttpClient(CIO)

/**
* https://api.payapp.kr/oapi/apiLoad.html
*
* cmd=payrequest&userid=payapptest&goodname=testGood&price=1000&recvphone=01055559999&smsuse=n
*
*/
suspend fun requestKtorIoInDetail(): String =
withContext(Dispatchers.IO) {
// set HttpRequestBuilder
val response: HttpResponse = client.request("https://api.payapp.kr/oapi/apiLoad.html") {
method = HttpMethod.Post
headers {
append("Content-Type", "application/x-www-form-urlencoded")
}
parameter("cmd","payrequest")
parameter("userid","payapptest")
parameter("goodname","testGood")
parameter("price","1000")
parameter("recvphone","01055559999")
parameter("smsuse","n")
}
val responseStatus = response.status
Log.d(TAG, "requestKtorIo: $responseStatus")

if (responseStatus == HttpStatusCode.OK) {
response.bodyAsText()
} else {
"error: $responseStatus"
}
}
}

개발자 가이드에서 말하는 것처럼 thread을 지원하여 사용자는 앱이 멈춤을 느끼지 않도록 하고 비동기 통신을 이용하여 목적에 필요한 자료를 전송하고 결과를 수집할 수 있습니다.

https://developer.android.com/kotlin/coroutines-adv?hl=ko







Kotlin 코루틴으로 앱 성능 향상  |  Android 개발자  |  Android Developers


Kotlin 코루틴으로 앱 성능 향상 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Kotlin 코루틴을 사용하면 네트워크 호출이나 디스크 작업과 같은 장기 실행 작


developer.android.com




payapp 결제 연동


payapp api 가이드에서 설명하는 결제 연동 페이지로의 호출 부분은 위 예시된 코드와 같이 url을 호출하면 됩니다. 이번에 구현하고자 하는 앱의 경우는 결제 연동만 일단 구현해 볼 요량이므로 위 코드와 같이 구현하고, 관련된 정보를 추가로 파라미터로 전송하여 결제 연동을 위한 QRcode 이미지를 webView에 보여주는 방식으로 진행하고 있습니다.

































파라미터 전달 값
cmdpayrequest (고정값 : 결제 요청시 사용)
useridpayapp 에 판매 회원으로 가입한 id
goodname판매하는 상품명칭
price가격
recvphone결과를 수신받을 휴대전화번호
smsusesms 사용여부

위 정도는 필수값이고 그 외에도 다른 항목들이 있지만 그것은 API 문서를 참조하여 보시기 바랍니다. 이번 앱 구현에서는 이 정도만 있어도 됩니다.


MainActivity에서 사용


class MainActivity : ComponentActivity() , CoroutineScope {

.....
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job

...

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

sp = getSharedPreferences(packageName, MODE_PRIVATE)
job = Job() // CoroutineScope를 위해 job을 할당

...

}

.....

private fun doPunched(destinationsNavigator: DestinationsNavigator) {
var errno = ""
var qrUrl = ""
launch(Dispatchers.Main) {
val result = HttpRequestHelper().requestKtorIoInDetail()
val decoded = URLDecoder.decode(result, "UTF-8")
var lineItems = decoded?.split("&")
if (lineItems != null) {
for (line in lineItems) {
Log.e("", "${line}")
if (line.indexOf("errno") > -1) {
errno = line.split("=")[1]
}
if (line.indexOf("qrurl") > -1) {
qrUrl = line.split("=")[1]
}
}
}
Log.e(TAG, "${qrUrl} ${errno}")
if (!"".equals(qrUrl)) {
doWebView(qrUrl, destinationsNavigator)
}
}
}

.....

fun doFinish() {
job.cancel() // 종료 되게...
finish()
}

mainActivity에서 다른 구현이 많이 있기는 하겠지만, 코루틴 사용을 위해서 Job을 선언하는 부분 등 필요한 부분만 나열을 하였으니 코드 구현 시 참고하셔야 합니다. 위 예시 코드와 같이 launch 구문을 이용해서 위에서 작성한 helper을 호출하면 결과 값이 string으로 돌아오기 때문에 그 값을 파싱 해서 qrurl 부분만 값으로 받아오고 그 값을 전달해서 webview에 표시하는 것으로 구현은 마무리가 됩니다.


@Composable
fun Url2WebView(
qrUrl : String,
doBackQRCode:() -> Unit
){

val fontFamily = FontFamily(Font(R.font.poorstory_regular, FontWeight.Normal))
Column(
modifier = Modifier
.fillMaxSize()
.padding(20.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Row(
modifier = Modifier.padding(2.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = {
doBackQRCode()
}) {
Icon(
imageVector = Icons.Outlined.ArrowBack,
contentDescription = "ArrowBack",
tint = softBlue
)
}
Text(text = stringResource(id = R.string.msgPunchedQRCode), style = TextStyle(
fontWeight = FontWeight.Bold, color = softBlue, fontSize = 24.sp,
fontFamily = fontFamily
))
}
// Adding a WebView inside AndroidView
// with layout as full screen
AndroidView(factory = {
WebView(it).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
webViewClient = WebViewClient()
loadUrl(qrUrl)
}
}, update = {
it.loadUrl(qrUrl)
})
}
}


이제 앱이 잘 구현 되기를 바라면 글을 정리해 보겠습니다.





오늘의 이야기



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

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

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

그것도 구글 Gemini로다가!

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

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

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


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




오늘의 이야기

이번 개발을 시작하면서 준비해야 할 것 중에 하나가 결제처리를 지원하는 PG(Payment GateWay) 연동 이었습니다. 이전 글에서도 적었던 것처럼 일반적인 PG 연동 API 들은 하나 같이 쇼핑몰 앱을 기반(?)으로 구동하는 것을 전제로 구현됩니다.

하지만, 이번에 구현해 보고 싶었던 앱은 결제를 중개해 주는 기능을 구현해 보고자 했습니다. 그래서 찾다 찾은 것은 PAYAPP 이라는 앱입니다.
https://payapp.kr/homepage/about/about.html







페이앱 공식 홈페이지


페이앱 본사, 블로그마켓 카드결제, 수기결제, 블로그결제, SMS결제, 블로그페이, 본사 고객센터 1800-3772


www.payapp.kr




* 아직은 이 회사와 아무런 관계가 없음을 미리 말해 둡니다.

단지, 구현해 보고자 했던 방향과 맞는 것 같아서 찾은 것일 뿐입니다

결제를 위한 QRCode


구현해 가는 방향은 이렇게 QRCode 을 보여 주면 고객은 휴대폰의 QRcode 스캐너, 카메라 앱을 이용해서 해당 링크로 넘어갑니다. 그러면 고객의 휴대폰에 다음과 같이 결제을 유도하는 창이 나타납니다.

결제요청페이지


이 페이지를 이용해서 결제는 고객의 휴대폰에서 진행됩니다. 결제가 완료되면 주문이 전달되는 그런 형태의 앱을 구현해 보고 있는 중입니다.





오늘의 이야기


#스하리1000명프로젝트

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

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

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

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





오늘의 이야기

바탕화면 이미지


이 꽃은 국화꽃이 형상화된 것 같습니다. 어느 날 퇴근 후에 노트북을 열었더니, 보였습니다.

이미지만 으로도 슬픔과 또 다른 어떤 느낌이 와닿았습니다.

어느 토요일 카카오의 데이터 센터 화재로 인해 몇일이 어떻게 지났는지 모르게 지났습니다

현장에서 대응하시는 분들의 속이 타 들어 가는 것을 다 알지는 못하지만, 나름 같은 IT업으로 먹고살고 있는 지라, 혹시 우리 센터에서도 저런 일이 벌어지면 대응이 잘 될까 하는 생각이 드는 일이기에, 연일 뉴스에 나오는 것처럼 말을 할 수는 없었습니다.

아무튼 오늘도 수고 하였으니, 편안한 밤이 되시길 바라며...





오늘의 이야기

https://github.com/afollestad/material-dialogs



 


GitHub - afollestad/material-dialogs: 😍 A beautiful, fluid, and extensible dialogs API for Kotlin & Android.


😍 A beautiful, fluid, and extensible dialogs API for Kotlin & Android. - GitHub - afollestad/material-dialogs: 😍 A beautiful, fluid, and extensible dialogs API for Kotlin & Android.


github.com




오늘은 앱을 구현하는 동안 간혹 사용하여야 하는 dialog box 구현에 대한 이야기를 해 보겠습니다.  위 링크에서 가져온 자료를 참고하여 구현하는 이야기를 해 볼 예정입니다. 


 


일반적인 dialog 구현해 보기


gradle 설정


먼저 gradle 설정을 해 보겠습니다.  먼저 봐야할 부분은 위 github의 게시글에서 보는 것처럼 dialog box의 여러 가지 형태 중에 어떤 것을 구현할 것인가를 정해야 할 것 같습니다. 


 


일반적인 diaglog box 예시



이런 모양의 일반적인 dialog box의 구현 부터 보겠습니다. 


 


// 다이얼로그
implementation 'com.afollestad.material-dialogs:core:3.3.0'
implementation 'com.afollestad.material-dialogs:lifecycle:3.3.0'

gradle 파일의 설정은 이 2줄만 가져 오면 됩니다.


 


Activity 구현


 


다음은 버튼을 표시하는 MainActivity 의 함수 하나를 보겠습니다. 


override fun onBackPressed() {
MaterialDialog(this@MainActivity).show {
icon(R.drawable.ic_bespeak_foreground)
title(R.string.titleFinish)
message(R.string.mesgFinish)
positiveButton(R.string.OK) { doFinish() }
negativeButton(R.string.Cancel) { this.dismiss() }
}
}

MaterialDialog를 구현한 것인데요. icon 은 말 그대로 알림 창에 icon 이 나옵니다  title 은 제목을 쓸 내용을 정리하면 되고요 message 에는 알림 표시용 글을 표시하면 되고, positiveButton 에는 확인 버튼을 구현하기 위한 메시지 내용과 버튼 클릭 시 동작할 action을 구현합니다. negativeButton 에는 취소 버튼을 구현하기 위한 메시지 내용과 action을 구현합니다. 


 


입력이 있는 Dialog 구현해 보기


gradle 설정


이번에는 입력이 있는 dialog 을 구현해 보겠습니다. 먼저 gradle 설정에 추가 하여야 합니다.


// 입력 받는 다이얼로그
implementation 'com.afollestad.material-dialogs:input:3.3.0'
implementation 'com.google.android.material:material:1.6.1'

저 2줄중에 위에 꺼는 원작자 링크에서 도 찾을 수 있었지만, 아래 줄은 없었습니다. 저 한 줄 때문에 에러가 나와서 찾는 데 애를 쫌 먹었습니다. 아무튼... 그리고 이번에는 style도 잘 정리가 되어야 하는 데, 결론적으로는 아래와 같이 theme.xml 파일을 정리했습니다.  원인은 Theme.AppCompat 이 아닐 경우 위에서 implementation 된 material layout이 동작하지 않는 것으로 보입니다.


 


Theme


그래서 꼭 theme 도 정리를 하고 시작 하시길 바랄게요.


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

<style name="Theme.bespeak1003" parent="Theme.AppCompat.DayNight">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/softBlue</item>
<!-- Customize your theme here. -->
<item name="colorAccent">@color/softBlue_30</item>
<item name="android:textColor">@color/softBlue</item>
<item name="android:fontFamily">@font/poorstory_regular</item>
<item name="android:textSize">20sp</item>
<item name="android:windowBackground">@color/softYellow</item>
<item name="android:colorBackground">@color/softGreen</item>
<item name="android:background">@color/rime200</item>
<item name="background">@color/softBlue</item>
</style>

</resources>

 


Activity 구현


입력 줄이 있는 dialog는 아래처럼 구현을 하였는 데, 일단 간결하기는 합니다. 다만, layout 이용해서 하기 때문에 위에서 처럼 theme 설정을 해야 하고, 입력받는 항목이 1개만 지원이 되고 있어서 코드 구현을 다채롭게 할 수는 없었습니다.


private fun doLoginAuth() {
val typeEmail = InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
val typePassword = InputType.TYPE_TEXT_VARIATION_PASSWORD
var userEmail = ""
var userPassword = ""
MaterialDialog(this@MainActivity).show {
icon(R.mipmap.ic_bespeak)
title(R.string.titleLogin)
message(R.string.msgEnterEmail)
input(inputType = typeEmail) { diaglog, text ->
userEmail = text.toString()
}
positiveButton(R.string.OK) {
toAction()
}
negativeButton(R.string.Cancel) {
it.dismiss()
}

}
}

입력을 한 개만 받는 부분은 개선(?) 요구를 해 볼까 하는 생각입니다.


입력이 있는 dialog 예시



배색을 잘하지 못해서 구분이 좀 어렵기는 하지만 위 그림처럼 파란 줄에 입력을 하나 받습니다. 그 입력받는 내용을 위 구현에서 보면 input { dialog , text ->  }처럼 구현해서 text로 그 값을 받아 와서 사용할 수 있습니다.  


 


이렇게 dialog 구현을 해 보았습니다. 더 쉬운 방법을 알게 되면 다시 구현해 보겠습니다.


 


 





오늘의 이야기


#스하리1000명프로젝트,
A volte è difficile parlare con i lavoratori stranieri, vero?
Ho realizzato una semplice app che aiuta! Scrivi nella tua lingua e gli altri lo vedono nella loro.
Si traduce automaticamente in base alle impostazioni.
Super pratico per chat facili. Dai un'occhiata quando ne hai la possibilità!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

오늘은 앱을 구현하는 과정에서 숫자를 입력받을 수 있는 TextField을 구현해 보도록 하겠습니다. 물론 이 부분도 힌트를 얻기 위해서 구글링을 했으며, 필요한 부분만 수정을 해 보았습니다. 


 


코드 구현



import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.sp
import com.billcoreatech.bespeak1003.R
import com.billcoreatech.bespeak1003.ui.theme.softBlue

@Composable
fun NumberField(
labelText : Int,
holderText : Int,
value: Number?,
onNumberChange: (Number) -> Unit,
) {
var fontFamily = FontFamily(Font(R.font.poorstory_regular, FontWeight.Normal))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
label = {
Text ( text = stringResource(id = labelText), style = TextStyle(
fontWeight = FontWeight.Bold, color = softBlue, fontSize = 24.sp,
fontFamily = fontFamily
))
},
placeholder = {
Text( text = stringResource(id = holderText))
},
value = value?.toString() ?: "",
onValueChange = {
it.toDoubleOrNull()?.let { value ->
if (value % 1.0 == 0.0) {
onNumberChange(value.toInt())
} else {
onNumberChange(value)
}
}
},
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}

TextField 는 일반적으로 String type으로 입력받고 결과도 String type으로 처리되는 것처럼 보입니다. 그래서 내부적으로 코드를 구현하여 integer, double type 등으로 형 변환을 처리하는 방식으로 처리를 하였습니다. 


 


구현된 화면


결과적으로는 숫자를 입력 받는 기본적은 입력창이 생기게 됩니다.  실행된 예시는 다음과 같은 화면 구성이 됩니다. 


 


입력 예시



 


개선 필요 사항


구글링을 통해서 찾은 코드를 이용해서 일부만 수정을 해 보았더니, 개선이 필요해 보이는 부분이 있는데, 


먼저는 숫자를 입력 받을 때는 오른쪽 정렬이 필요해 보이는 현상이 있습니다. 그리고 다음은 3자리마다 콤마를 표현하는 부분이 있으면 좋을 것 같다는 생각이 듭니다. 


 


기회가 되면 기능 구현을 해 볼 까 합니다.


 





오늘의 이야기

이 글은 이전 포스팅에서 연속됩니다. 


https://billcorea.tistory.com/238



 


안드로이드 앱 만들기 : navigation 을 편리하게 ? (두번째 이야기)


https://flatteredwithflutter.com/using-compose-destinations%ef%bf%bc/ Using compose destinations We will cover briefly: Current navigation in composeUsing compose destinations(Optional) Modify exi..


billcorea.tistory.com




이전 글에서 작성했던 navigation 은 그저 화면의 이동에 중점을 두었습니다. 그러다 보니, 화면 하단에 메뉴를 달고 그 메뉴는 항상 표시가 되어야 했지만, 그렇지 못했습니다 그래서 이번에는 꼭 하단 메뉴는 그대로 두고 content 내용만 변경되는 모양으로 만들어 보기로 했습니다. 


 


다시 원작자가 작성한 코드를 살펴보면서 따라 하기를 해 보았습니다.  먼저 참조할 소스 코드의 github 링크는 아래와 같습니다. 


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



 


GitHub - raamcosta/compose-destinations: Annotation processing library for type-safe Jetpack Compose navigation with no boilerpl


Annotation processing library for type-safe Jetpack Compose navigation with no boilerplate. - GitHub - raamcosta/compose-destinations: Annotation processing library for type-safe Jetpack Compose na...


github.com




여기에서 참조되는 코드를 이용해서 내가 작성할 코드를 작성해 보았습니다.


 


Bottombar


이 코드는 화면 하단에 보여줄 bottom menu 항목을 정하고,  그것들을 보여주는 작성을 합니다. 


사용할 단위 화면을 먼저 작성해 두고 이전 포스팅에서 설명한 바와 같이./gradlew clean build 명령어를 이용하여 기본 컴파일은 진행되어야 합니다. 


 


아래 코드에서 BottombarItem으로 선언된 Home, ProductItem, Setting 등이 그 예시입니다.




import androidx.annotation.StringRes
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.billcoreatech.bespeak1003.R
import com.billcoreatech.bespeak1003.ui.theme.fonts
import com.billcoreatech.bespeak1003.ui.theme.softBlue
import com.billcoreatech.bespeak1003.widget.NavGraphs
import com.billcoreatech.bespeak1003.widget.destinations.DirectionDestination
import com.billcoreatech.bespeak1003.widget.destinations.HomeScreenDestination
import com.billcoreatech.bespeak1003.widget.destinations.ManagerScreenDestination
import com.billcoreatech.bespeak1003.widget.destinations.ProductItemScreenDestination
import com.ramcosta.composedestinations.navigation.navigate
import com.ramcosta.composedestinations.navigation.popBackStack
import com.ramcosta.composedestinations.navigation.popUpTo
import com.ramcosta.composedestinations.utils.isRouteOnBackStack

@Composable
fun BottomBar(
navController: NavHostController
) {
BottomNavigation {
BottomBarItem.values().forEach { destination ->

val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

BottomNavigationItem(
alwaysShowLabel = true,
selected = currentRoute == destination.direction.route ,
onClick = {
navController.navigate(destination.direction) {
popUpTo(NavGraphs.root) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
Icon(
destination.icon,
contentDescription = stringResource(destination.label)
)
},
label = { Text(
text = stringResource(destination.label),
style = TextStyle(
fontFamily = fonts,
color = Color.White,
fontSize = 18.sp,
fontWeight = FontWeight.Normal
)) },
)
}
}
}

enum class BottomBarItem(
val direction: DirectionDestination,
val icon: ImageVector,
@StringRes val label: Int
) {
Home(HomeScreenDestination, Icons.Outlined.Home, R.string.Home),
ProductItem(ProductItemScreenDestination, Icons.Outlined.ShoppingCart, R.string.productItems),
Setting(ManagerScreenDestination, Icons.Outlined.Settings, R.string.Setting)
}

코드를 가져오면서 잠깐의 혼돈은 onClick에 구현된 코드 작성에서 있었습니다. import 처리가 잘 되지 않아서 코드를 직접 입력하면서 해소를 하였습니다.


BespeakScaffold


이 부분은 MainActivity에서 호출한 Scaffold을 변형한 코드입니다.  원작자의 코드에서는 topbar 도 있었지만, 이번에는 사용을 하지 않을 것이기 때문에 제거하고 필요한 부분만 가져왔습니다.



import android.util.Log
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
import androidx.navigation.plusAssign
import com.billcoreatech.bespeak1003.widget.appCurrentDestinationAsState
import com.billcoreatech.bespeak1003.widget.destinations.Destination
import com.billcoreatech.bespeak1003.widget.startAppDestination
import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi
import com.google.accompanist.navigation.material.ModalBottomSheetLayout
import com.google.accompanist.navigation.material.rememberBottomSheetNavigator
import com.ramcosta.composedestinations.spec.Route

@OptIn(ExperimentalMaterialNavigationApi::class)
@Composable
fun BespeakScaffold(
startRoute: Route,
navController: NavHostController,
bottomBar: @Composable (Destination) -> Unit,
content: @Composable (PaddingValues) -> Unit,
) {
val destination = navController.appCurrentDestinationAsState().value
?: startRoute.startAppDestination

// 👇 디버깅을 위해서만 주석에 의해 제한되기 때문에 backQueue API를 사용해서는 안됩니다.
navController.backQueue.print()

val bottomSheetNavigator = rememberBottomSheetNavigator()
navController.navigatorProvider += bottomSheetNavigator

// 👇 ModalBottomSheetLayout은 일부 대상이 하단 시트 스타일인 경우에만 필요합니다.
ModalBottomSheetLayout(
bottomSheetNavigator = bottomSheetNavigator,
sheetShape = RoundedCornerShape(16.dp)
) {
Scaffold(
bottomBar = { bottomBar(destination) },
content = content
)
}
}

private fun ArrayDeque<NavBackStackEntry>.print(prefix: String = "stack") {
val stack = map { it.destination.route }.toTypedArray().contentToString()
Log.e("ArrayDeque","$prefix = $stack")
}

 


MainActivity


MainActivity는 setContent 부분만 기술했습니다.  다른 부분들은 이번 포스팅과 연관이 없기 때문에요.  위에서 작성한 BespeakScaffold을 호출하고, DestinationNavHost 코드만 기술하는 것으로 해서 코드 작업은 마무리되었습니다.


setContent {

val engine = rememberAnimatedNavHostEngine()
val navController = engine.rememberNavController()
val startRoute = NavGraphs.root.startRoute

Bespeak1003Theme(darkTheme = isSystemInDarkTheme()) {
BespeakScaffold(
navController = navController,
startRoute = startRoute,
bottomBar = {
BottomBar(navController)
}
) {
DestinationsNavHost(
engine = engine,
navController = navController,
navGraph = NavGraphs.root,
modifier = Modifier.padding(it),
startRoute = startRoute
)
}
}
}

 


다음에 비슷한 구현을 하게 되더라도 이런 정도의 코드 작업을 선행해 두면 bottom navigation의 구현은 어렵지 않게 구현이 될 것으로 생각이 됩니다.


 


예시 화면



 


다음에 수정된 내용이 있으면 수정하도록 하겠습니다.


 


전체코드는 다음에서 참고하세요.


 


https://github.com/nari4169/bottom-navigation-sample



 


GitHub - nari4169/bottom-navigation-sample: first


first. Contribute to nari4169/bottom-navigation-sample development by creating an account on GitHub.


github.com




 





오늘의 이야기

오늘은 영문 번역에 대한 이야기를 잠깐 해 볼까 합니다.  아직 테스트가 원활하지는 않지만, kakao에서 지원했던 번역 API는 2022.07.01 사용이 중지되어 기존 허용되었던 api 사용자에게는 2023.06.30까지만 지원한다고 합니다. 


아무튼 현재 무료로 사용할 수 있는 번역 API는 구글, naver, kakao 등이 있습니다. 


 


kakao가 지원하는 다음 버전의 번역API는 cloud에 포함되면서 개인이 사용하기에는 조금 더 장벽(?)이 있는 것처럼 보입니다. 좀 더 알아보아야겠지만요.


 


아무튼 코드는 다음과 같이 구현을 해 보았습니다.


 


# POST /v2/translation/translate HTTP/1.1
# Host: dapi.kakao.com
# Authorization: KakaoAK {REST_API_KEY}
# Content-type: application/x-www-form-urlencoded

# curl "https://openapi.naver.com/v1/papago/n2mt" \
# -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
# -H "X-Naver-Client-Id: ..............." \
# -H "X-Naver-Client-Secret: ..........." \
# -d "source=ko&target=en&text=만나서 반갑습니다." -v

import os
import requests
import docx2txt
import urllib.request
import json

baseURL = 'https://dapi.kakao.com/v2/translation/translate'
naverURL = 'https://openapi.naver.com/v1/papago/n2mt'
apiKey = '....................' # 카카오 API 을 위한 키값
# '.................'
trLng = 'kr'
basePath = 'D:/'

def translateNaver(pStr):
client_id = "............" # 개발자센터에서 발급받은 Client ID 값
client_secret = "........" # 개발자센터에서 발급받은 Client Secret 값
encText = urllib.parse.quote(pStr)
data = "source=en&target=ko&text=" + encText
url = naverURL
request = urllib.request.Request(url)
request.add_header("X-Naver-Client-Id", client_id)
request.add_header("X-Naver-Client-Secret", client_secret)
response = urllib.request.urlopen(request, data=data.encode("utf-8"))
rescode = response.getcode()
if (rescode == 200):
response_body = response.read()
jsonStr = json.loads(response_body.decode('utf-8'))
return jsonStr['message']['result']['translatedText']
else:
print("Error Code:" + rescode)
return rescode

def translateKakao(pStr):
headers = {'Content-type': 'application/x-www-form-urlencoded', 'Authorization': 'KakaoAK ' + apiKey}
parameter = {'query': pStr, 'src_lang': 'en', 'target_lang': trLng}
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
req = requests.post(baseURL, data=parameter, headers=headers)
json_str = ''
if req.status_code == 200:
json_str = req.json()['translated_text']
json_str = str(json_str[0]).replace('[', '')
json_str = json_str.replace(']', '')
json_str = json_str.replace("'", "", 2)
else:
json_str = req.json()['message']
return json_str


def docx_to_txt(path, filename):
text = docx2txt.process(path + filename).split('\n')
# docx 파일의 내용이 text에 담기는데 '\n' 줄바꿈 문자 기준으로 나뉘어 리스트로 만들어 진다.
newfile = os.path.splitext(filename)[0] + '.txt'
f = open(os.path.join(path, newfile), 'w', encoding='UTF-8') # txt 파일 오픈(없으면 생성됨)
for item in text: # text에 담긴 [[Example01], [Hello], [Hi] ...]의 요소 하나가 txt파일에 한줄로 써지게 됨.
f.write(item + '\n')
f.close()
return newfile # {file}.txt가 리턴됨


newFile = docx_to_txt(basePath, '내문서파일.docx')
print(newFile)

f = open(os.path.join(basePath, newFile), 'r', encoding='UTF-8')
fw = open(os.path.join(basePath, 'outFile.txt'), 'w', encoding='UTF-8')

for line in f:
if len(line.strip()) < 1:
pass
else:
print("[" + line.strip() + "]" + str(len(line.strip())))
if len(line.strip()) != 0:
transStr = translateKakao(line.strip())
print(transStr)
fw.write(transStr)
break

fw.close()
f.close()

print('Job END...')

 


이상의 코드는 docx 문서 파일에 영문 원본이 있다고 하는 경우 그 내용을 일단 text 파일로 변환을 하고 해당 text 파일에서 본문 내용을 읽어서 번역 API을 호출해 보는 방식입니다.


 


kakao을 호출하는 부분과 naver 을 호출 하는 부분을 따로 함수를 만들어 보았는데, 물론 구글링을 해서 얻은 소스들입니다.  일정 부분 테스트를 거치면서 수정이 되기는 했지만요. 


 


또 하나는 kakao는 번역을 지원하는 쿼터가 1일 50000자, 네이버는 10000자의 제한이 있습니다.  그 이상이 되면 번역을 해 주지 않습니다. ㅠㅠ;;


 


구글도 번역 api가 있는 기 한데, 설정에 대한 이해가 아직이라 알게 되면 정리를 다시 해 보겠습니다.


 





오늘의 이야기

앱에서 사용되는 결제 모듈 (PG 연동)의 경우는 대부분이 내 상점에서 결제를 요청하는 방식에 포커스가 맞게 되어 있어, 이를 이용하는 것은 맞지 않는 것 같습니다.

다만, 그래도 알아보았던 내용은


Tosspayment


https://www.tosspayments.com/







토스페이먼츠


결제를 간편하게, 사업은 더 편하게


www.tosspayments.com




대강 살펴본 바로는 java script 중심의 지원을 하는 것으로 보입니다. 사용을 해 보면 좋을 것 같기는 합니다. 다만, 서버가 없는 상태에서 사용을 구현해 보기에는 아직 그 정도의 스킬은 없어서...


Bootpay


https://www.bootpay.co.kr/







무료 결제 연동 API 서비스


개발자를 위한 PG 연동 서비스로, 빠른 결제 연동, 더 높은 결제성공율, 무료 통계 서비스를 제공합니다.


www.bootpay.co.kr




이 또한 연동을 위한 API 지원이나, 여러 가지 자료가 있고, 구글링을 했을 때 추천하는 글을 많이 보았습니다. 실제 구현을 해 보기에는 어떤지 아직 감이 오지 않습니다. 다른 여러 PG사와 연동을 지원하는 것이 매력일 듯 해 보이나, 실제 비용이 청구되는 시점에 어떤 생각이 들지는 체험을 해 보지 못해 알 수 없습니다.

다른 결제 수단들의 연동이 Bootpay 을 경유(?) 모양이 로만 구글링의 결과가 보이기 때문에 일단...

여기서 자료 조사는 그만해야 할 것 같습니다.


Payapp


https://payapp.kr/homepage/index.html







페이앱 공식 홈페이지


페이앱 본사, 블로그마켓 카드결제, 수기결제, 블로그결제, SMS결제, 블로그페이, 본사 고객센터 1800-3772


www.payapp.kr




연동하는 방식에 qrcode 을 통한 결제를 지원이 되는 것을 확인 했어요.


결론


이번에 구현하고자 하는 앱의 결제 모듈 연동은 제외 대상이 되었습니다. 그래야 목표했던 앱의 구현에 쉽게 도달할 수 있을 것으로 생각이 되기 때문입니다.


To be continued





오늘의 이야기

지난 포스팅에 연속해서 https://billcorea.tistory.com/267 개발일기 #5 PAYAPP 연동을 위한 준비 이번 개발을 시작하면서 준비해야 할 것 중에 하나가 결제처리를 지원하는 PG(Payment GateWay) 연동 이었습니...