2026/03/21

오늘의 이야기

이 앱은 사용자의 휴대폰에 있는 사진(이미지)만 백업을 해 두고 싶을 때 사용할 수 있는 앱입니다.  이 앱 사용하기 위한 전제 조건은 2가지입니다. 
 

  1. WIFI 을 사용할 수 있는 가 ?
  2. NAS을 사용할 수 있거나, FTP 서버를 이용할 수 있는 가?

 
이 2가지 조건을 가지고 있지 않다면 그냥 이 앱을 지우고 벗어나시길 바랍니다. 
 

대표 이미지


 
이제 시작해 보겠습니다.  먼저 설정을 해 보겠습니다. 

기본화면


 
** 서버 접속 준비 하기
입력해야 하는 것은 서버 IP을 입력합니다.  앱에서는 동일한 WIFI 에서 사용할 수 있는 NAS의 FTP을 사용하는 것을 기준으로 앱을 구현하였기 때문에 IP 주소를 입력받습니다. 
 
사용자 ID와 패스워드는 NAS 의 서버 설정에 해당하는 ID 와 PASSWORD을 입력하면 됩니다. 
 
서버 경로는 FTP로 접속했을 때 경로를 찾아야 합니다.  그래서 windows 사용자의 경우에는 command 창을 열어서
 
ftp을 실행하고
open 192.168.0.7처럼 입력 하고 
user / password을 입력해 접속하여 경로를 찾아봅니다.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

FTP 접근해 보기


이 그림과 같이 서버 접속을 직접 해 보아야 하는 것은 서버 경로를 NAS의 설정에서 어떻게 하는 가에 따라서 달라지기 때문 이기도 합니다.   접속 경로가 확인되면 그 경로를 위 설정 하면에 서버 경로에 입력해 주면 됩니다. 
 


설정 버튼을 이용하여  입력한 서버 정보를 저장해 주어야 합니다.   이 작업이 되지 않은 경우 정상적인 진행을 할 수 없을 수 도 있습니다. 
 
이제 다음은 백업 주기를 설정 합니다.  구글에서는 배터리의 안정적인 운영을 위해서는 15분 이상으로 백그라운드 작업을 하도록 가이드하고 있습니다. 

그런데도 간격의 범위를 3분 ~ 50분으로 설정한 것은 필요에 따라서 설정을 달리 해야 하는 경우가 있을 수 도 있기 때문입니다. 
 
시간 설정까지를 마무리했다면 이제 백업 버튼을 이용하여 백업을 진행해 봅니다. 


백업버튼을 클릭했을 때 다른 부분은 
 
15분 미만으로 설정한 경우에는  백업 버튼을 클릭하기 전에  리스트 보기를 활성화해서 사진 목록이 조회되는 것으로 확인해야 합니다. 
 
이미지 목록이 없을 경우에는 알림이 표시됩니다. 
 
15분 이상으로 설정한 경우에는 백그라운드 반복 작업을 하기 때문에 리스트가 보이지 않아도 진행을 가능합니다. 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
이제 어떻게 동작을 하는지 예시 동영상을 한번 보겠습니다. 
 
 

실행하는 예시 동작

간혹 휴대장치의 성능 이슈 때문에 프로그래스바의 동작이 확인되지 않는 경우가 발생하기는 합니다.  프로그래스바 가 표시 되고 있는 동안은 전송건수에 숫자가 변하는 것을 확인할 수 있습니다. 
 
사라지는 사진을 백업해 보세요.
 
 
https://play.google.com/store/apps/details?id=com.billcorea.ftpclient0710 

사진백업 - Google Play 앱

NAS 로 내 폰의 사진을 백업해 드립니다. 자동으로 해 드릴 생각 이에요

play.google.com

 





오늘의 이야기

앱을 구현하는 동안 FTP 접속을 통해서 파일을 서버로 전송하는 앱을 구현해 보기로 하겠습니다. 


 


먼저 gradle 파일에 아래 항목을 추가합니다. FTP 서버를 활용하기 위해서는 필요합니다.


implementation 'commons-net:commons-net:3.9.0'

 


코드 구현에 필요한 코드는 아래와 같습니다. 이 명령줄을 이용해서  접속할 서버 정보를 이용해 파일을 보내 보는 것입니다.



import org.apache.commons.net.ftp.FTP
import org.apache.commons.net.ftp.FTPClient
import java.io.File
import java.io.FileInputStream

fun uploadFileToFtp(file: File, ftpServer: String, ftpUsername: String, ftpPassword: String, ftpDirectory: String) {
val ftpClient = FTPClient()
ftpClient.connect(ftpServer)
ftpClient.login(ftpUsername, ftpPassword)
ftpClient.enterLocalPassiveMode()
ftpClient.setFileType(FTP.BINARY_FILE_TYPE)
ftpClient.changeWorkingDirectory(ftpDirectory)

val inputStream = FileInputStream(file)
val fileName = file.name
ftpClient.storeFile(fileName, inputStream)
inputStream.close()

ftpClient.logout()
ftpClient.disconnect()
}

 


서버 접속 설정해 보기



화면에서는 서버 접속을 위한 정보를 입력하여 이 정보를 이용해서 FTP 서버에 접속해 보겠습니다.  그런데 일반적으로는 FTP 서버를 가지고 있지는 않을 것입니다.  그래서 저는 NAS을 활용해 보기로 했습니다. 


 


iptime NAS 의 FTP 서비스 정보 설정 하기



ip Time NAS 을 사용하는 경우이기 때문에 자신이 가지고 있는 것을 이용해서 설정을 해야 합니다.  



  • 포트번호는 기본값이 21입니다.  왜 21 이냐고 물어보신다면 그건 구글에서 찾아보시길 추천해 드립니다.

  • 모든 사용자를 익명으로 사용은 체크하지 않습니다. 그래야 사용자 관리에서 입력한 사용자를 선택해서 id와 password을 입력해 볼 수 있기 때문입니다. 

  • 다음은 공유폴더를 하나 선택해서 FTP에서 사용할 폴더로 지정해 주어야 합니다. 여기서 선택하는 경로를 실제 FTP client 프로그램에서 접속해 보려면 실제 접속해서 확인을 해 보아야 합니다. 


ftp 접속해 보기



windows의 경우 cmd 창을 열어서 ftp을 실행해 봅니다.  그다음과 같은 순서로 서버에 접속해 봅니다. 



  • open 192.168.0.7 ( 서버 IP는 NAS의 아이피와 동일합니다)

  • 사용자 이름 입력 

  • password 입력

  • dir을 입력해서 현재 경로를 확인합니다. 


위 그림과 같이 로그인 후 바로 확인되는 경로는 HDD1입니다. 위 설정에서는 BackupDocument였는 데 말입니다. 그래서  HDD1에 접근하고 나서 다시 확인 보면 이제야 해당 경로가 나오는 것을 알 수 있습니다.  그래서 경로 확인은 꼭 접속해 확인해 보아야 하는 이유입니다.


 


이상으로 오늘은 FTP 접속하는 방법과 kotlin 코드를 알아보았습니다.


 





오늘의 이야기


#스하리1000명프로젝트,
Lost in Korea? Même si vous ne parlez pas coréen, cette application vous aide à vous déplacer facilement.
Parlez simplement votre langue : il traduit, recherche et affiche les résultats dans votre langue.
Great for travelers! Prend en charge plus de 10 langues, dont l'anglais, le japonais, le chinois, le vietnamien et plus encore.
Try it now!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/03/20

오늘의 이야기

## 로또 번호 분석 및 추천

**데이터 분석 요약:**

총 20회차 데이터를 분석하여 각 회차별 번호 패턴을 파악하고, 이를 바탕으로 다음 회차 번호를 예측합니다. 분석 항목은 다음과 같습니다.

1. **연속 번호 간 간격:** 각 회차의 번호들 간 오름차순 간격.
2. **짝수/홀수 개수:** 각 회차의 짝수 및 홀수 번호 개수.
3. **총합:** 각 회차 번호 6개의 합계 및 동일 총합 출현 간격.
4. **평균:** 각 회차 번호 6개의 평균값 및 동일 평균값 출현 간격.
5. **매칭 점수:** 각 회차의 1~4번 항목 패턴을 이전 회차들과 비교하여 산출한 매칭 비율(%) 및 동일 매칭 비율 출현 간격.
6. **번호 출현 빈도:** 전체 데이터에서 각 번호가 출현한 총 빈도수.

---

**상세 분석 결과:**

* **전체 평균 총합:** 20회차 동안 번호 6개의 평균 총합은 약 **205.83**입니다. (최소 157, 최대 264)
* **전체 평균 번호 간 간격:** 번호들 간의 평균 간격은 약 **6.84**입니다.
* **가장 빈번한 짝수/홀수 패턴:** (3 Even, 3 Odd) - 10회 출현 (50%)

**지난 회차(1215회) 분석:**

* **당첨 번호:** [13,15,19,21,44,45]
* **간격:** (2,4,2,23,1)
* **짝수/홀수:** (2 Even, 4 Odd)
* **총합:** 157
* **평균:** 26.17

---

**최종 추천 번호 및 상세 근거:**

다음 회차(1216회)를 위한 5가지 추천 조합입니다. 모든 조합은 지난 10회차 당첨 번호와 중복되지 않으며, 1부터 45 사이의 숫자로 구성됩니다.

### 1. 추천 [01,27,31,35,38,41]
* **선정 근거:** 지난 20회차 동안 가장 많이 출현한 6개의 숫자(1, 27, 38, 31, 35, 41)를 조합했습니다. 통계적으로 가장 높은 출현 빈도를 가진 숫자들로 구성되어 있습니다. 이 조합의 총합은 173으로, 전체 평균 총합(205.83)보다 낮아 최근 총합 트렌드(157)와 유사하거나 약간 높은 수준입니다. 짝수/홀수 비율은 (2 Even, 4 Odd)입니다.

### 2. 추천 [04,26,30,25,31,35]
* **선정 근거:** 지난 회차(1215회)의 짝수 2개, 홀수 4개 패턴에서 벗어나 가장 빈번하게 나타나는 짝수 3개, 홀수 3개의 균형 패턴으로 회귀할 것을 예측하여 구성했습니다. 최다 빈도수 숫자 풀에서 짝수 3개와 홀수 3개를 선택하여 구성되었습니다. 이 조합의 총합은 171, 평균은 28.5입니다.

### 3. 추천 [06,16,24,32,36,40]
* **선정 근거:** 지난 회차의 총합(157)이 전체 평균 총합(약 205.83)보다 낮았으므로, 평균 총합(205.83)에 가까운 약 200~220 범위의 숫자로 구성했습니다. 번호 간 평균 간격도 전체 평균 간격(약 6.84)과 유사하게 (4,8,8,4,4)로 배치하여 극단적인 패턴을 피했습니다. 짝수 6개, 홀수 0개의 짝수 강세 패턴입니다.

### 4. 추천 [10,18,22,28,38,42]
* **선정 근거:** 지난 회차의 번호들이 주로 중소 번호대에 집중된 경향(13,15,19,21)이 있었고 짝수 2, 홀수 4였습니다. 이를 역전하여 짝수 6개, 홀수 0개의 비율로, 번호 범위도 중후반대에 고루 분포되도록 큰 숫자들을 포함하여 구성했습니다. 총합은 158로 지난 회차와 유사하며, 평균은 26.33으로 소폭 상승했습니다.

### 5. 추천 [03,12,17,20,33,43]
* **선정 근거:** 최근 10회차 당첨 번호에서 출현 빈도가 다소 낮지만, 전체적으로는 꾸준히 출현하는 중간 빈도수의 숫자들과 최다 빈도수 숫자를 혼합했습니다. 번호 간 간격이 너무 좁거나 넓지 않도록 (9,5,3,13,10) 적절히 배분하여 다양한 범위의 숫자를 커버하도록 했습니다. 총합은 128로 낮은 편이며, 짝수 2개, 홀수 4개 조합입니다.

---

**지난 회차(1215회) 분석과 추천 조합 비교:**

| 항목 | 1215회 당첨 번호 | 추천 1번 [01,27,31,35,38,41] | 추천 2번 [04,26,30,25,31,35] | 추천 3번 [06,16,24,32,36,40] | 추천 4번 [10,18,22,28,38,42] | 추천 5번 [03,12,17,20,33,43] |
| :------------- | :-------------------- | :-------------------------- | :-------------------------- | :-------------------------- | :-------------------------- | :-------------------------- |
| **당첨 번호** | [13,15,19,21,44,45] | [01,27,31,35,38,41] | [04,25,26,30,31,35] | [06,16,24,32,36,40] | [10,18,22,28,38,42] | [03,12,17,20,33,43] |
| **총합** | 157 | 173 | 171 | 194 | 158 | 128 |
| **평균** | 26.17 | 28.83 | 28.5 | 32.33 | 26.33 | 21.33 |
| **짝수/홀수** | 2 Even, 4 Odd | 2 Even, 4 Odd | 3 Even, 3 Odd | 6 Even, 0 Odd | 6 Even, 0 Odd | 2 Even, 4 Odd |
| **간격 패턴** | (2,4,2,23,1) | (26,4,4,3,3) | (21,1,4,1,4) | (10,8,8,4,4) | (8,4,6,10,4) | (9,5,3,13,10) |
| **주요 특징** | 낮은 총합, 중소 번호대, 높은 간격 | 고빈도수, 낮은 총합, 일부 큰 간격 | 균형 잡힌 짝홀, 낮은 총합, 고빈도수 | 짝수 강세, 평균 총합, 고른 간격 | 짝수 강세, 낮은 총합, 중대 번호 | 고빈도/저빈도 혼합, 낮은 총합, 다양한 간격 |

**비교 분석 요약:**

* **총합 및 평균:** 지난 1215회차의 총합(157)과 평균(26.17)이 전체 평균보다 낮았기 때문에, 추천 조합들은 이와 유사한 낮은 총합/평균을 유지하거나(추천 1, 2, 4, 5) 전체 평균에 가까운 총합/평균(추천 3)을 가지도록 구성했습니다.
* **짝수/홀수 균형:** 1215회차는 (2 Even, 4 Odd) 패턴을 보였습니다. 추천 조합들은 이 패턴을 유지하거나(추천 1, 5), 가장 빈번한 (3 Even, 3 Odd) 패턴(추천 2), 또는 반전 패턴인 (6 Even, 0 Odd) 패턴(추천 3, 4)을 제시하여 다양한 가능성을 고려했습니다.
* **번호 간 간격:** 1215회차는 (2,4,2,23,1)과 같이 '23'이라는 매우 큰 간격이 특징적이었습니다. 추천 조합들은 이처럼 극단적인 간격보다는 전체 평균 간격(약 6.84)에 가깝거나, 특정 구간에 집중되는 경향을 보이도록 구성했습니다. 예를 들어, 추천 3번은 비교적 고른 간격(10,8,8,4,4)을 가지며, 추천 5번은 큰 간격(13,10)을 일부 포함하지만 1215회차의 23만큼 극단적이지는 않습니다.
* **번호대 분포:** 1215회차는 [13,15,19,21]과 같은 중소 번호와 [44,45]의 큰 번호가 혼합된 형태였습니다. 추천 조합들은 최다 빈도수 숫자를 기반으로 하되, 중소 번호, 중대 번호, 고른 분포 등 다양한 번호대 조합을 시도하여 예측의 폭을 넓혔습니다.



사용하는 예시 영상 보기
이 앱이 궁금 하다면, 아래 링크에서 설치할 수 있습니다.
로또 645






오늘의 이야기

오늘은 앱의 하단  탐색바를 만들다 고민이 생긴 걸 해소하는 이야기를 적어 봅니다.  


https://medium.com/@banmarkovic/jetpack-compose-clear-back-stack-popbackstack-inclusive-explain ed-14ee73a29df5



 


Jetpack Compose clear back stack, popBackStack inclusive explained


Understand the popUpTo and inclusive parameter for navigation-compose lib, and learn how to clear back stack.


medium.com




이 이야기의 출처는 위에 링크를 참조해 주세요.   이야기의 중요 논점은 내비게이션 바를 이용해서 화면 간의 이동을 하게 되는 경우 어떻게 뒤로 가기를 정리할 것인가 하는 문제와 만나게 되면서 시작됩니다. 


 


bottom bar navigation 은 stack에 메뉴을 하나씩 넣어 주면서 화면을 이용 합니다. stack 이라는 것이 FILO 라는 규칙을 따라가기 때문에 차곡 차곡 쌓아 두었다가 하나씩 빠져 나가는 것으로 처리를 하게 됩니다. 


 


popbackStack 의 처리 예시



위 그림과 같이  stack 에 Login, Home, Profile이라는 화면을 하나씩 선택해 들어갔을 때 어떻게 해서 정리를 해 두면 좋을까 하는 부분에 대한 고민이 생겨 납니다. 


 


사용자들은 앱을 사용하다가 뒤로 가기 버튼을 클릭하는 것으로 앱을 종료하려고 합니다.  (물론 그냥 닫아 버리기도 하기는 합니다.) 그랬을 때 스택에 들어 있는 것들이 많았다면 화면을 하나하나씩 빼내어 뒤로 가기를 실행하다가 stack에서 다 빠지게 되면 그때서야 앱을 종료하는 onBackPress Call Back을 타게 됩니다. 


 


그래서 저는 저 이야기를 읽고 나서 앱의 bottom bar navigation을 구현하면서 다음과 같이 탐색하는 방법을 변경해 보았습니다. 


 



dropletButtons.forEachIndexed { index, it ->
DropletButton(
modifier = Modifier.fillMaxSize(),
isSelected = dataViewModel.selectedItem.value == index,
onClick = {
Log.e("", "index = $index")
dataViewModel.selectedItem.value = index
naviController.navigate(it.direction) {
popUpTo(naviController.graph.id) {
inclusive = true
}
}
},
icon = it.icon,
size = 26.dp,
contentDescription = stringResource(id = it.description),
dropletColor = Color.White,
animationSpec = tween(durationMillis = Duration, easing = LinearEasing)
)
}


navigate(다음화면) { popUpTo(화면 ID) { inclusive = true } } 이렇게 호출하는 것으로 해서 stack에 들어가는 것들을 지우면서 화면 이동을 시도 했습니다.   결국에는 stack 에 저장된 것이 마지막 하나뿐인 상태가 되는 것입니다. 


 


그랬더니, 뒤로 가기 버튼을 클릭하면 바로 onBack Press Call Back으로 들어옵니다.  이제 한 번에 뒤로 가기 버튼을 이용한 앱 종료 액션을 처리해 볼 수 있었습니다. 


 


오늘도 즐~ 코딩...


 


이제 얼마 남지 않은 장맛비를 잘 달래 보내야 할 때가 된 것 같습니다.





오늘의 이야기

오늘만에 제대로 된 개발 이야기를 적어 보겠습니다.  한동안 외주 프로젝트에 참여를 하고 있어서 마음에 여유가 없었던 탓이기도 하고, 게으른 탓이기도 하겠지만, 개발이야기는 적어 보지 못했습니다.   그동안 눈팅(?)하던 jetpack compose 이야기에서 애니메이션이 가미된 navigation bar을 만들어 보겠습니다. 


 


https://medium.com/proandroiddev/jetpack-compose-tutorial-animated-navigation-bar-354411c679c8



 


Jetpack Compose Tutorial: Animated Navigation Bar


How to implement a navigation bar with smooth custom animations


proandroiddev.com




 원작자의 이야기는 여기에서 보실 수 있습니다.  영문 버전으로 이해를 해야 하기에 나름의 고충(?)도 있을 수 있으나, 이런 정도는 번역기가 잘 번역을 해 줍니다. 


 


이글의 이야기들을 읽다 보면 마치 무슨 수학 함수를 풀어내야만 하는 것 처럼 보이기도 합니다.  아무튼... 다이내믹한 뭔가를 얻어 내는 데, 무한한 노력이 필요해 보이기는 합니다. 


 


//AnimatedNavigationBar
implementation("com.exyte:animated-navigation-bar:1.0.0")

애니메이션 네비게이트를 활용하기 위해서 gradle 파일에 추가를 했습니다. 


 


이번에는 메뉴에 표시할 항목등을 만들어 보도록 하겠습니다.  여러개를 만들어 낼 수 도 있지만, 예시에서는 메뉴 항목이 5개인 것을 구현해 보겠습니다.


 


import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Stable
import com.billcorea.bluetooth627.R

@Stable
data class Item(
@DrawableRes val icon: Int,
var isSelected: Boolean,
@StringRes val description: Int,
val animationType: ColorButtonAnimation = BellColorButton(
tween(500),
background = ButtonBackground(R.drawable.plus)
),
)

val dropletButtons = listOf(
Item(
icon = R.drawable.home,
isSelected = false,
description = R.string.Home
),
Item(
icon = R.drawable.bell,
isSelected = false,
description = R.string.Bell
),
Item(
icon = R.drawable.message_buble,
isSelected = false,
description = R.string.Message
),
Item(
icon = R.drawable.heart,
isSelected = false,
description = R.string.Heart
),
Item(
icon = R.drawable.outline_menu_24,
isSelected = false,
description = R.string.Menu
),

)

이렇게 메뉴에 들어갈 항목을 만들어 보았습니다. 다만, 여기서 알 수 없는 것은 BellColorButton 이 위에서 implemention에서 가져오지 못한 다는 것입니다.  알 수 없는 일이기도 합니다.  아무튼 그것에 관련된 코드 부분은 github에 있는 project 파일에서 가져왔음을 밝혀 둡니다. 


 


import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import com.exyte.animatednavbar.utils.rotationWithTopCenterAnchor
import kotlin.math.PI
import kotlin.math.sin

class BellColorButton(
override val animationSpec: FiniteAnimationSpec<Float> = tween(),
override val background: ButtonBackground,
private val maxDegrees: Float = 30f,
) : ColorButtonAnimation(animationSpec, background) {

@Composable
override fun AnimatingIcon(
modifier: Modifier,
isSelected: Boolean,
isFromLeft: Boolean,
icon: Int,
) {
val rotationFraction = animateFloatAsState(
targetValue = if (isSelected) 1f else 0f,
animationSpec = animationSpec,
label = "rotationFractionAnimation"
)

val color = animateColorAsState(
targetValue = if (isSelected) Color.Black else Color.LightGray,
label = "colorAnimation"
)

Icon(
modifier = modifier
.rotationWithTopCenterAnchor(
if (isSelected) degreesRotationInterpolation(
maxDegrees,
rotationFraction.value
) else 0f
),
painter = painterResource(id = icon),
contentDescription = null,
tint = color.value
)
}

private fun degreesRotationInterpolation(maxDegrees: Float, fraction: Float) =
sin(fraction * 2 * PI).toFloat() * maxDegrees
}

import androidx.annotation.DrawableRes
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset

data class ButtonBackground(
@DrawableRes val icon: Int,
val offset: DpOffset = DpOffset.Zero
)

@Stable
abstract class ColorButtonAnimation(
open val animationSpec: FiniteAnimationSpec<Float> = tween(10000),
open val background: ButtonBackground,
) {
@Composable
abstract fun AnimatingIcon(
modifier: Modifier,
isSelected: Boolean,
isFromLeft: Boolean,
icon: Int,
)
}

이렇게 이번  project 에서 가져다 사용할 것만 추려서 가져왔습니다.  하단바 메뉴를 구현해 보겠습니다.


 


import android.util.Log
import android.view.animation.OvershootInterpolator
import androidx.annotation.StringRes
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.billcorea.bluetooth627.DoubleDuration
import com.billcorea.bluetooth627.Duration
import com.billcorea.bluetooth627.R
import com.billcorea.bluetooth627.composes.destinations.AuthorSettingScreenDestination
import com.billcorea.bluetooth627.composes.destinations.DirectionDestination
import com.billcorea.bluetooth627.composes.destinations.MainScreenDestination
import com.billcorea.bluetooth627.ui.theme.Purple
import com.billcorea.bluetooth627.ui.theme.SoftBlue
import com.billcorea.bluetooth627.ui.theme.SoftBlue40
import com.exyte.animatednavbar.AnimatedNavigationBar
import com.exyte.animatednavbar.animation.balltrajectory.Parabolic
import com.exyte.animatednavbar.animation.indendshape.Height
import com.exyte.animatednavbar.animation.indendshape.shapeCornerRadius
import com.exyte.animatednavbar.items.dropletbutton.DropletButton
import com.ramcosta.composedestinations.navigation.navigate

@Composable
fun BottomBar( naviController : NavHostController) {

var selectedItem by remember { mutableStateOf(0) }
AnimatedNavigationBar(
modifier = Modifier
.padding(horizontal = 4.dp, vertical = 4.dp)
.height(90.dp),
selectedIndex = selectedItem,
barColor = SoftBlue,
ballColor = SoftBlue,
cornerRadius = shapeCornerRadius(25.dp),
ballAnimation = Parabolic(tween(Duration, easing = LinearOutSlowInEasing)),
indentAnimation = Height(
indentWidth = 56.dp,
indentHeight = 15.dp,
animationSpec = tween(
DoubleDuration,
easing = { OvershootInterpolator().getInterpolation(it) })
)
) {
dropletButtons.forEachIndexed { index, it ->
DropletButton(
modifier = Modifier.fillMaxSize(),
isSelected = selectedItem == index,
onClick = {
Log.e("", "index = $index")
selectedItem = index
when (selectedItem) {
0 -> {
naviController.navigate(BottomMenuItems.Main.direction)
}
4 -> {
naviController.navigate(BottomMenuItems.Setting.direction)
}
else ->{

}
}
},
icon = it.icon,
contentDescription = stringResource(id = it.description),
dropletColor = Color.White,
animationSpec = tween(durationMillis = Duration, easing = LinearEasing)
)
}
}
}

enum class BottomMenuItems(
val direction : DirectionDestination,
@StringRes val label : Int,
) {
Main(MainScreenDestination, R.string.Main),
Setting(AuthorSettingScreenDestination, R.string.Setting),
}

 


아직은 테스트를 위한 것이라 메뉴가 2개만 들어 있는 것 같은 느낌인 데, 실행해 보면 다음과 같이 구현이 되는 것을 알 수 있습니다.


 


 



애니메이션 네비게이트


이렇게 오늘도 따라 하기 코딩 하나를 해 보았습니다.  조금 아쉬운 부분은 네비게이션 구현을 함에 있어서 메뉴에 label 이 나오지 않는다는 것이 하단바 메뉴로 사용하기에는 부족한 부분이 아닐까 하는 생각을 해 보았습니다.   아무튼 오늘도 끝.


기본 이미지



 





오늘의 이야기



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

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

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

그것도 구글 Gemini로다가!

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

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

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


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




오늘의 이야기

https://www.bing.com/images/create/ed9598eb8a98ec8389-ed8c8ceb9e80-ebb094eb8ba4/6491a919f8d5477487bb29934c8d184f?id=BxWtxOGiLVXzQoaxOvpYUw%3D%3D&view=detailv2&idpp=genimg&idpclose=1&form=SYDBIC 



 


Bing


Bing은 지능적인 검색 기능은 사용자가 원하는 정보를 빠르게 검색하고 보상을 제공합니다.


www.bing.com






이제 여기저기에서 AI Chat Bot을 사용하게 되는 것 같습니다. windows을 사용하시는 분들이라면 아마도 update 된 bing을 사용하실 수 도 있을 것 같네요.


 


파란 하늘을 닮은 바다를 그려 줄 수 있는 지 물었습니다. 보시는 것과 같은 그림 4장을 만들어 주더군요.  하늘과 맞닿은 바다가 그립습니다. 


 


수평선 저 끝에는 무엇이 있을까요?  신대륙을 발견 했던 콜럼버스의 마음으로 이제 떠나고 싶다는 생각이 듭니다. 


파란 바다가 그리운 저녁 입니다.


 





오늘의 이야기


#스하리1000명프로젝트

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

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

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

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





오늘의 이야기

오늘은 챗봇에게 질문을 했습니다. 타원형  progress bar를그려 보자고...


 


동그란 progress bar 구현



같은 지점에서 출발해서 2개의 입력이 동시에 그려지는 progress bar 그리기입니다.   이 하나의 그림을 얻기 위해서 질문을 여러 번 해야 했습니다. 


 


1. Kotlin으로 타원형 진행률 표시줄을 그리는 코드 만들기


2. 하나의 그래프에 두 개의 서로 다른 입력을 받는 진행률을 그릴 수 있습니까?


3. 이 코드는 오류가 없습니까?


4. 타워의 경계를 따라 진행 상황을 보여주는 kotlin 코드


5. 두 개의 입력을 받고 진행률을 동시에 표시하도록 kotlin 코드 수정


6. canvas.drawPath를 사용하여 다시 만듭니다.


7. 놀이터 트랙처럼 보이는 타원에 트랙을 따라 동일한 지점에서 시작하여 두 개의 진행률을 표시하는 kotlin 코드를 작성합니다.


8. 하단이 아닌 테두리에만 표시해야 합니다.


9. 진행률이 테두리를 따라 곡선으로 표시되도록 만들어주세요


10. 바닥면은 도색 안해도 되나요?


11. 바닥은 아직 칠하고 있지만 칠하고 싶지 않습니다.


12. 둥근 타원 그래프


13. 약간 더 넓은 원형 타원 그래프


 


이렇게 13번의 수정 질문을 던져서 받은 코드입니다.


import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View

class DualOvalProgressBar(context: Context, attrs: AttributeSet) : View(context, attrs) {

private val progressPaint1: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val progressPaint2: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val progressPath1: Path = Path()
private val progressPath2: Path = Path()

private var progress1: Float = 0f
private var progress2: Float = 0f
private var maxProgress: Float = 100f

init {
progressPaint1.color = Color.BLUE
progressPaint2.color = Color.RED
progressPaint1.style = Paint.Style.STROKE
progressPaint2.style = Paint.Style.STROKE
progressPaint1.strokeWidth = 40f
progressPaint2.strokeWidth = 40f
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

val centerX = width / 2f
val centerY = height / 2f
val horizontalRadius = Math.min(width, height) / 2f - progressPaint1.strokeWidth / 2f
val verticalRadius = horizontalRadius / 2f

// Calculate the sweep angles for the progress arcs
val progressSweepAngle1 = 360f * progress1 / maxProgress
val progressSweepAngle2 = 360f * progress2 / maxProgress

// Calculate the start and end angles for the progress arcs
val startAngle1 = -90f
val endAngle1 = startAngle1 + progressSweepAngle1
val startAngle2 = endAngle1
val endAngle2 = startAngle2 + progressSweepAngle2

// Create the progress paths
progressPath1.reset()
progressPath1.addArc(
centerX - horizontalRadius,
centerY - verticalRadius,
centerX + horizontalRadius,
centerY + verticalRadius,
startAngle1,
progressSweepAngle1
)
progressPath2.reset()
progressPath2.addArc(
centerX - horizontalRadius,
centerY - verticalRadius,
centerX + horizontalRadius,
centerY + verticalRadius,
startAngle2,
progressSweepAngle2
)

// Draw the progress paths
canvas.drawPath(progressPath1, progressPaint1)
canvas.drawPath(progressPath2, progressPaint2)
}

fun setProgress1(progress: Float) {
this.progress1 = progress
invalidate()
}

fun setProgress2(progress: Float) {
this.progress2 = progress
invalidate()
}

fun setMaxProgress(maxProgress: Float) {
this.maxProgress = maxProgress
invalidate()
}
}

 


이렇게 class을 하나 만들어 내고 난 다음.  layout xml에 다음을 추가했습니다. 


 


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

<data>

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".MainActivity"
tools:ignore="HardcodedText,SpUsage,ContentDescription">


<!-- app:layout_constraintStart_toEndOf="@+id/guideLine"-->
<com.example.twowayprogressgraph.DualOvalProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_editor_absoluteX="20dp"
tools:layout_editor_absoluteY="20dp" />



</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 


이렇게 layout 을 만들고 mainActivity에서는 다음과 같이 호출하여 사용합니다. 


 


class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val progressBar: DualOvalProgressBar = findViewById(R.id.progressBar)
progressBar.setMaxProgress(100f)
progressBar.setProgress1(30f)
progressBar.setProgress2(70f)

}
}

 


이제 API을 통해서 결과를 받아오고 그것을 표시하는 progress bar을 구현해야 한다면... 이렇게 구현해 볼 수 있을 것 같습니다. 


 


https://github.com/nari4169/TwoWayProgressGraph



 


GitHub - nari4169/TwoWayProgressGraph


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


github.com




코드는 이렇게 공유를 해 보겠습니다.


 


 





오늘의 이야기

이 글은 앱을 이용하여 회원관리를 할 수 있도록 구성하기 위한 방편으로 관리자용 업무 기능 지원하는 앱 사용자를 위한 사용자 설명서 페이지입니다. 


 


회원관리를 위한 앱은 바코드 할인쿠폰으로  playstore에 게시되어 있으며, 이전 설명 자료를 참고해 보실 수 있습니다. 


https://billcorea.tistory.com/324



 


할인쿠폰 모아보기 앱 사용자 가이드 (Ver 0.5.0 변경분)


이 앱은... 이 앱은 최초에 마트에서 제공해 주는 할인쿠폰을 모아 보기 위해서 기획 의도 되고 개발된 앱 이었습니다. 어느 날 요청자 께서 말씀하시길 회원이 보여주는 영수증도 모아볼 수 있


billcorea.tistory.com




 


이제 부터는 관리자를 위한 앱 사용자 설명을 해 보도록 하겠습니다. 


바코드 할인쿠폰 (관리자용)

이 앱은 Ver 0.5.0부터 시작합니다. 사유는 회원관리용 앱을 통합해 관리를 했으나, 사용자 관점에서 불편한 부분들이 발생하고 있어서 앱의 역할을 명확하게 분리하고자 하는 생각이 있었습니다.


 



  • 설정 버튼 : 관리자의 정보 관리를 위한 버튼

  • 뒤로가기 : 앱을 종료하고자 할 때 사용

  • 추가하기 (+) : 현재는 관리자도 바코드 할인쿠폰을 등록할 수 있도록 지원

  • 영수증 관리 : 회원의 제시한 영수증을 관리할 수 있도록 지원

  • 영수증 목록 : 등록된 영수증 목록을 조회할 수 있도록 지원

  • 회원 목록 : 등록된 회원 리스트를 조회하고 알림을 전달할 수 있도록 지원

  • 대기자 목록 : 회원앱과 관리자 앱이 근처에서 활성화된 경우 회원 앱에서 방문 기록을 요청하면 등록되어 조회되도록 지원

  • 홍보성 이미지 관리 : 이곳에서 등록된 이미지는 관리자앱 근처에 나타나는 회원앱이 활성화 된 경우 공유 설정된 이미지를 전달할 수 있도록 지원


기본 버튼의 메뉴는 이상과 같이 관리가 됩니다.




화면 상단에 나타나는 버튼 이미지 입니다. 이 버튼 이미지가 나타났을 때 클릭 하면 관리자 설정에서 생성된 QRCode 을 근처에 도착한 회원 앱으로 전달하는 기능도 지원됩니다.


 


메인 화면



 


 


관리자 설정 (화면이미지의 버전 표시는 변경될 수 있습니다.)

관리자 설정이 필요한 이유는 회원 관리를 하기 위해서는 데이터 저장을 해야 하는 데, 관리를 위해서 온라인으로 진행하고자 합니다. 해서 관리자 설정을 해 주세요.
 






관리자 등록 화면에서는



  • 먼저 관리자로 사용된 이메일 ID와 비밀번호를 입력하고  관리자 추가를 하게 되면 다음부터는 관리자 확인만 가능합니다

  • 관리자 확인이 되고 나면 QRcode 위에 있는 공유 버튼을 클릭하여 회원 가입 링크를 공유합니다.  그 이후에는 화면에 QRcode 전달을 위한 링크가 생겨 납니다.

  • 그다음부터는 QRCode 을 이용해서 고객 가입을 유도할 수 있습니다.


** 관리자의 비밀번호를 분실한 경우에는 비밀번호 하단의 제일 오른쪽 버튼을 이용하여 등록한 이메일 계정으로 관리자 비밀번호 변경할 수 있는 링크를 전달할 수 있습니다.
*** 관리자를 삭제하고 다시 등록하는 경우에는 5회 이상 반복 하게 되면 해당 이메일 계정은 더 이상 등록할 수 없도록 제한될 수 있으니 주의가 필요합니다.
 


회원가입 링크 공유



회원 가입 유도를 위한 링크 공유는 SNS 또는 SMS , 이메일 등등 모든 공유 수단을 활용할 수 있습니다. 


공유방법 선택



공유하는 방법을 선택하는 경우 '한 번만', '항상' 중에서 선택할 수 있는 데, 이 경우



  • 한 번만 하는 경우에는 다음에 또 같은 선택을 해야 합니다.

  • 항상을 선택하는 경우에는 다음번부터 같은 공유 방법으로만 할 수 있기 때문에 주의가 필요합니다. (물론 휴대폰의 설정에서 변경할 수 있습니다.)


** 더 쉬운 방법은 상대방이 앞에 있다면 위 그림의 QRCode 을 상대방의 휴대폰 카메라에 보여주는 것이 제일 수월 합니다.  이 경우에는 상대방 휴대폰 카메라앱에서 바로 링크가 나타납니다. (다만, 안드로이드 폰에서만 가능합니다.)
 


회원으로 가입이 유도되는 경우 다음과 같이 화면에 변경됩니다.   (회원앱의 설명에 근거해 확인해 주세요)

회원 가입 링크를 클릭해 실행된 경우



회원 화면으로 실행된 경우에는  회원의 이름과 연락처만 입력하고 저장하게 되고, 회원 스스로 할인 쿠폰을 관리하고자 하는 경우에는 할인 쿠폰을 등록해 보관 관리 할 수 있도록 지원됩니다.  
 


영수증 등록 하기 

영수증 등록해 보기



영수증 등록은 회원정보를 먼저 확인한 이후에 저장 버튼이 활성화됩니다. (저장 버튼의 색갈이 파란색이 됩니다.  준비 시점에는 저장 버튼 색갈이 검은색입니다.)
 
다음은 영수증 이미지를 선택하여야 합니다. 


이미지 선택하기 방법



영수증 이미지 선택 방법은 카메라 직접 촬영을 하거나, 이미 촬영된 이미지를 읽어오는 방법을 선택할 수 있습니다. 
가져온 영수증 이미지에서 필요한 부분만 선택적으로 자르기를 할 수 있도록 지원하고 있습니다. 사진 전체에서 이미지를 선택하고 자르기 버튼을 클릭하면 화면에 선택된 부분의 이미지만 가져오는 것을 볼 수 있습니다.


이미지 자르기 전 후



이미지 선택이 된 이후에  회원 정보 위에 있는 문자 인식 버튼을 클릭하여 화면의 내용 중에서 문자로 인식된 부분을 조회해 볼 수 있습니다. 


문자 인식된 내용중에서 해당하는 금액 선택 하기



인식된 문자들 중에서 숫자 변환될 수 있는 부분의 내용을 표시하고 있으니 그 부분에서 해당금액이 있는 부분을 선택하면 거래 금액이 표시 되게 됩니다.  (** 이 경우에는 거래금액을 수정할 수 없습니다.)
 


영수증 목록 보기

저장된 이후에는 영수증 목록 보기에서 저장된 목록을 확인할 수 있습니다.  영수증 목록에서는 회원의 연락처 정보를 이용해서 해당 해당 회원만 검색할 수 있도록 지원되고 있습니다.
 


저장된 영수증 목록 보기



 


고객 리스트 조회

 
고객명단은 회원으로 가입된 고객의 리스트를 조회할 수 있도록 지원됩니다. 고객이 많은 경우 이름의 첫 글자부터 한 글자 이상 입력 후 검색해 볼 수 있습니다. 
 


고객 명단 조회



또한 해당 고객에게 알림을 보내고자 하는 경우 간단한 문구를 입력하여 고객이 설치한 앱으로 푸시 알림을 보낼 수 있도록 지원됩니다. 


알림 메시지 전송



알림 메시지를 전송하게 되면 고객의 휴대폰으로 푸시 알림이 전송되며, 고객이 알림을 선택하면 고객화면으로 전달된 알림 내용을 확인해 볼 수 있습니다.  고객 화면의  위 부분에는 그동안 수신된 안내 메시지가 나오게 되어 있으며, 아래 부분에는 인식된 영수증의 리스트를 조회할 수 있도록 하고 있습니다.
 


고객 화면 알림 및 영수증 내역 조회



 


대기자 목록 조회

 


대기자 목록 조회는 회원앱과 관리자 앱이 근처(100m 이내)에서 활성화되는 경우 회원앱도 나타나는 근처 인식 버튼을 이용하여 관리자 앱으로 대기 신청을 했을 때 등록된 정보를 조회할 수 있는 화면입니다. 


대기리스트 연결 순서



이 그림과 같이 관리자앱과 회원앱이 근처에서 활성화되면 상단에 버튼 이미지가 추가로 나타납니다. 이때 회원 앱에서 해당 버튼을 클릭하면 관리자 앱으로 회원 정보를 전달하게 됩니다. 도달이 완료 되면 대기자 리스트에 추가 되며, 회원앱으로는 대기자 리스트에 등록 되었다는 알림을 전달 하게 됩니다.


 


 


광고 이미지 등록 관리

광고 이미지 등록 관리 화면에서


광고 이미지 등록 예시



이 화면에서 관리되는 홍보성 이미지는 대기자 목록 조회에서 처럼 근접한 회원 앱으로 자동 전달 됩니다.  회원은 해당 이미지를 보고 삭제할 수 도 있지만, 전달하고자 하는 의도는 구현이 될 듯합니다.


 


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



 


바코드 할인쿠폰 (관리자) - Google Play 앱


이 앱은 바코드 할인쿠폰 앱과 연동 하여 회원관리를 지원 하기 위한 관리자 전용 입니다.


play.google.com




 


앱을 설치해 보시면 회원관리가 수월해 집니다.





오늘의 이야기

이전 글에서 정리할 것처럼 java에서 kotlin으로 이전을 했습니다.  그러고 나서 보기 시작했는 데,  DefaultSharedPrefernces의 사용할 수 없는 환경으로 변경이 된 것을 알게 되었습니다.    이전 prefs = Prefere...