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




 


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





오늘의 이야기


#스하리1000명프로젝트,
Đôi khi thật khó để nói chuyện với người lao động nước ngoài phải không?
Tôi đã tạo một ứng dụng đơn giản có ích! Bạn viết bằng ngôn ngữ của bạn và những người khác nhìn thấy nó bằng ngôn ngữ của họ.
Nó tự động dịch dựa trên cài đặt.
Siêu tiện dụng để trò chuyện dễ dàng. Hãy xem khi bạn có cơ hội!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

Certainly! Here's some information about Seoul in 2023:

1. Major Events: Seoul is a vibrant city that hosts numerous events throughout the year. In 2023, you can expect various cultural festivals, music concerts, art exhibitions, and sporting events. Some notable events might include the Seoul Lantern Festival, Seoul International Music Festival, Seoul Fashion Week, and the Seoul Marathon. Make sure to check the local event calendars for specific dates and details.

2. Attractions: Seoul offers a wide range of attractions to explore. You can visit historical landmarks like Gyeongbokgung Palace, Changdeokgung Palace, and Bukchon Hanok Village to experience the city's rich history and traditional architecture. Don't miss the bustling markets such as Namdaemun Market and Dongdaemun Market, known for their shopping and street food. Other popular sites include N Seoul Tower, Myeongdong shopping district, and the Han River Park.

3. Technology and Innovation: Seoul is renowned for its advancements in technology. You can explore high-tech neighborhoods like Gangnam and Digital Media City, where you'll find futuristic buildings, cutting-edge technology, and innovation centers. Don't forget to visit the famous Samsung d'light exhibition center to see the latest gadgets and innovations.

4. Cuisine: Seoul is a food lover's paradise, offering a diverse range of culinary delights. You can savor traditional Korean dishes like bibimbap, bulgogi, and kimchi, as well as enjoy international cuisines in the city's many restaurants. Don't miss the chance to experience street food at popular spots like Gwangjang Market and Myeongdong, where you can try tasty treats like tteokbokki (spicy rice cakes) and hotteok (sweet pancakes).

5. Transportation: Seoul has a well-developed public transportation system, making it easy to navigate the city. You can utilize the subway, buses, and taxis to travel to different parts of Seoul conveniently. Additionally, Seoul offers bike-sharing services and rental cars for more flexibility in exploring the city.

Please note that the above information is based on the current knowledge available up until September 2021, and there might be additional developments and changes in the city by 2023. It's always a good idea to check for the latest information and updates closer to your travel dates.


 


틀림없이! 2023년 서울에 대한 정보는 다음과 같습니다. 1. 주요 행사: 서울은 연중 수많은 행사가 열리는 활기찬 도시입니다. 2023년에는 다양한 문화제와 음악회, 미술 전시회, 스포츠 행사를 기대할 수 있습니다. 서울빛초롱축제, 서울국제음악제, 서울패션위크, 서울마라톤 등이 대표적이다. 특정 날짜 및 세부 정보는 지역 행사 일정을 확인하십시오. 2. 관광명소: 서울은 탐험할 수 있는 다양한 관광명소를 제공합니다. 경복궁, 창덕궁, 북촌 한옥마을 등의 역사적 명소를 방문하여 도시의 풍부한 역사와 전통 건축물을 경험할 수 있습니다. 쇼핑과 길거리 음식으로 유명한 남대문시장, 동대문시장 등 북적이는 시장을 놓치지 마세요. 다른 인기 명소로는 N서울타워, 명동 쇼핑가, 한강공원 등이 있습니다. 3. 기술과 혁신: 서울은 기술 발전으로 유명합니다. 미래형 건물, 최첨단 기술, 혁신 센터가 있는 강남과 디지털 미디어 시티와 같은 첨단 기술 지역을 탐험할 수 있습니다. 유명한 Samsung d'light 전시 센터를 방문하여 최신 장치와 혁신을 보는 것을 잊지 마십시오. 4. 요리: 서울은 다양한 종류의 요리를 제공하는 미식가의 천국입니다. 비빔밥, 불고기, 김치와 같은 전통 한식을 맛볼 수 있을 뿐만 아니라 도시의 많은 레스토랑에서 세계 각국의 요리를 즐길 수 있습니다. 떡볶이, 호떡 등 맛있는 음식을 맛볼 수 있는 광장시장, 명동 등 인기 명소에서 길거리 음식을 경험할 기회를 놓치지 마세요. 5. 교통: 서울은 대중교통이 잘 발달되어 있어 이동이 편리합니다. 지하철, 버스, 택시를 이용하여 서울의 여러 지역으로 편리하게 이동할 수 있습니다. 또한 서울은 자전거 공유 서비스와 렌터카를 제공하여 보다 유연하게 도시를 둘러볼 수 있습니다. 위의 정보는 2021년 9월까지 사용 가능한 현재 지식을 기반으로 하며 2023년까지 도시에서 추가 개발 및 변경이 있을 수 있습니다. 여행 날짜에 가까운 최신 정보 및 업데이트를 항상 확인하는 것이 좋습니다..


 


이상은 gpt가 말해준 2023년의 서울 이야기입니다.


Gpt3.0은 2021년까지의 정보만 있다는 걸 스스로 말해 주네요.





오늘의 이야기

오늘은 앱을 구현하는 동안 알게 된 Nearby Connections API에 대한 이야기를 정리해 두고자 합니다.  이 기능은 wifi 가 되지 않더라도 주변(10m 반경)에 같은 앱을 사용하는 사용자가 있을 때, 데이터, 텍스트, 이미지 등을 공유하는 기능을 구현하기 위해서 사용됩니다. 


 


https://medium.com/androiddevelopers/two-way-communication-without-internet-nearby-connections-b1185 30cb84d



 


Two-way communication without internet: Nearby Connections (Part 2 of 3)


The Nearby Connections API allows your users to connect with each other even when no internet is available. The API uses Bluetooth and…


medium.com




참고글의 링크 하나를 달아 둡니다.  읽어 보면 도움이 될 듯합니다.   그리고 안드로이드 개발자 매뉴얼은 다음 링크를 참조하였습니다. 


 


https://developers.google.com/nearby/connections/android/get-started?hl=ko 



 


시작하기  |  Nearby Connections  |  Google for Developers


메일링 리스트에 가입하여 Nearby에 관한 소식 및 공지사항을 확인하세요. 이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 시작하기 컬렉션을 사용해 정리하기 내 환경설정을


developers.google.com




 


이제 알아본 내용들을 기초로 하여 구현을 시도해 보겠습니다.  개발자 페이지 내용을 보다 보면 참고할 소스 코드가 github에 올라가 있는 것을 볼 수 있었습니다. 그것을  fork 하여 개인 계정으로 가져온 다음 일단은 그대로 저장해 실행을 해 보기록 했습니다. 


 


https://github.com/nari4169/connectivity-samples/tree/main/NearbyConnectionsWalkieTalkie



 


GitHub - nari4169/connectivity-samples: Multiple samples showing the best practices in connectivity on Android.


Multiple samples showing the best practices in connectivity on Android. - GitHub - nari4169/connectivity-samples: Multiple samples showing the best practices in connectivity on Android.


github.com




여러 개의 예제 소스가 있기는 했지만, 가장 필요한 기능에 유사한 것은 간단한 무선통신(워키토키)을 지원하는 코드가 눈에 들어와 실행을 해 보았습니다. 


 


다만, 원래 코드가 2018년쯤에 만들어진 이후 업데이트가 되지 않아서 최신 환경에 맞지 않는 부분들이 있었고, java 코드로 되어 있어 그대로 일단은 실행이 될 수 있도록 하는 부분만 수정해서 개인 계정에는 commit을 해 두었습니다.  저 링크에 있는 코드는 2023년 현재에도 그대로 실행해 볼 수 있는 코드입니다. 


 


저는 그것을 가지고 와서  kotlin을 수정된 코드를 활용해서 만들고 있는 앱에 적용해 보기로 했습니다.   아무튼 기능 구현을 해 보도록 하겠습니다. 


 


먼저 권한 설정을 시작합니다. 


 


manifest 파일에 다음과 같이 권한을 추가했습니다. 


    <!-- Required for Nearby Connections -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />

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

<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />

 


이제 기본으로 사용할 연결 작성을 위해서 ConnectionsActivity을 작성해 보겠습니다.  사실 이 코드는 개발자 페이지에 있는 코드를 kotlin을 변환한 코드입니다. java 코드 그대로라고 봐도 무방(?) 합니다. 


import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.CallSuper
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.billcorea.barcodevoucher0225.R
import com.billcorea.barcodevoucher0225.database.DBHandler
import com.billcorea.barcodevoucher0225.database.ViewModels
import com.billcorea.barcodevoucher0225.databean.ReceiveMessage
import com.google.android.gms.common.api.Status
import com.google.android.gms.nearby.Nearby
import com.google.android.gms.nearby.connection.AdvertisingOptions
import com.google.android.gms.nearby.connection.ConnectionInfo
import com.google.android.gms.nearby.connection.ConnectionLifecycleCallback
import com.google.android.gms.nearby.connection.ConnectionResolution
import com.google.android.gms.nearby.connection.ConnectionsClient
import com.google.android.gms.nearby.connection.ConnectionsStatusCodes
import com.google.android.gms.nearby.connection.DiscoveredEndpointInfo
import com.google.android.gms.nearby.connection.DiscoveryOptions
import com.google.android.gms.nearby.connection.EndpointDiscoveryCallback
import com.google.android.gms.nearby.connection.Payload
import com.google.android.gms.nearby.connection.PayloadCallback
import com.google.android.gms.nearby.connection.PayloadTransferUpdate
import com.google.android.gms.nearby.connection.Strategy
import com.google.android.gms.tasks.OnFailureListener
import com.google.firebase.auth.FirebaseAuth
import java.util.Locale

/** Nearby Connections에 연결하고 편리한 메서드 및 콜백을 제공하는 클래스입니다. */
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
abstract class ConnectionsActivity : ComponentActivity() {

private val dataViewModel : ViewModels by viewModels()

/**
* ConnectionsActivity 권한으로 앱에 필요한 모든 권한을 풀링하는 선택적 후크
*요청합니다.
*
* @return 앱이 제대로 작동하는 데 필요한 모든 권한입니다.
*/
/**
* 이러한 권한은 Nearby Connections에 연결하기 전에 필요합니다.
*/

protected open val requiredPermissions: Array<String> = arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_ADVERTISE,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
)

private val REQUEST_CODE_REQUIRED_PERMISSIONS = 1

private val wifiManager: WifiManager by lazy {
applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
}

/**
* 로깅을 위해 [Status]를 영어로 읽을 수 있는 메시지로 변환합니다.
*
* @param status 현재 상태
* @return 읽을 수 있는 문자열. 예. 파일을 찾을 수 없습니다.
*/
private fun toString(status: Status): String {
return String.format(
Locale.US,
"[%d]%s",
status.statusCode,
if (status.statusMessage != null) status.statusMessage else ConnectionsStatusCodes.getStatusCodeString(
status.statusCode
)
)
}

private fun hasPermissions(context: Context?, vararg permissions: String?): Boolean {
if (context != null) {
for (permission in permissions) {
logD("permission=$permission", "")
if (ActivityCompat.checkSelfPermission(
context,
permission!!
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
}
return true
}

/** Nearby Connections에 대한 핸들러입니다. */
lateinit var mConnectionsClient: ConnectionsClient

/** 우리 주변에서 발견한 장치. */
private val mDiscoveredEndpoints: MutableMap<String, Endpoint> = HashMap()

/**
* 보류 중인 연결이 있는 장치입니다. [ ][.acceptConnection] 또는 [.rejectConnection]을 호출할 때까지 보류 상태로 유지됩니다.
*/
private val mPendingConnections: MutableMap<String, Endpoint> = HashMap()

/**
* 현재 연결된 장치. 광고주의 경우 이는 클 수 있습니다. 발견자에게는
* 이 맵에는 하나의 항목만 있습니다.
*/
private val mEstablishedConnections: MutableMap<String, Endpoint?> = HashMap()
/** 현재 다른 장치에 연결을 시도하고 있으면 'true'를 반환합니다. */
/**
* 검색된 장치에 연결을 요청하는 경우 True입니다. 우리가 묻는 동안, 우리는 다른 것을 요청할 수 없습니다
* 장치.
*/
protected var isConnecting = false
private set
/** 현재 검색 중이면 'true'를 반환합니다. */
/** 발견하는 경우 참입니다. */
protected var isDiscovering = false
private set
/** 현재 광고 중인 경우 'true'를 반환합니다. */
/** 광고하는 경우 참입니다. */
protected var isAdvertising = false
private set

/** 다른 장치에 대한 연결을 위한 콜백. */
private val mConnectionLifecycleCallback: ConnectionLifecycleCallback =
object : ConnectionLifecycleCallback() {
override fun onConnectionInitiated(endpointId: String, connectionInfo: ConnectionInfo) {
logD(
String.format(
"onConnectionInitiated(endpointId=%s, endpointName=%s)",
endpointId, connectionInfo.endpointName
),
""
)
val endpoint = Endpoint(endpointId, connectionInfo.endpointName)
mPendingConnections[endpointId] = endpoint
this@ConnectionsActivity.onConnectionInitiated(endpoint, connectionInfo)
}

override fun onConnectionResult(endpointId: String, result: ConnectionResolution) {
logD(
String.format(
"onConnectionResponse(endpointId=%s, result=%s)",
endpointId,
result
),
""
)

// We're no longer connecting
isConnecting = false
if (!result.status.isSuccess) {
logW(
String.format(
"Connection failed. Received status %s.",
toString(result.status)
)
)
onConnectionFailed(mPendingConnections.remove(endpointId))
return
}
connectedToEndpoint(mPendingConnections.remove(endpointId))
}

override fun onDisconnected(endpointId: String) {
if (!mEstablishedConnections.containsKey(endpointId)) {
logW("Unexpected disconnection from endpoint $endpointId")
return
}
disconnectedFromEndpoint(mEstablishedConnections[endpointId])
}
}

/** 다른 장치에서 우리에게 보낸 페이로드(데이터 바이트)에 대한 콜백. */
private val mPayloadCallback: PayloadCallback = object : PayloadCallback() {
override fun onPayloadReceived(endpointId: String, payload: Payload) {
logD(
String.format("onPayloadReceived(endpointId=%s, payload=%s)", endpointId, payload),
""
)
onReceive(mEstablishedConnections[endpointId], payload)
}

override fun onPayloadTransferUpdate(endpointId: String, update: PayloadTransferUpdate) {
logD(
String.format(
"onPayloadTransferUpdate(endpointId=%s, update=%s)", endpointId, update
),
""
)
}
}

/** 활동이 처음 생성될 때 호출됩니다. */
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mConnectionsClient = Nearby.getConnectionsClient(this)
}

/** 활동이 사용자에게 표시될 때 호출됩니다. */
override fun onStart() {
super.onStart()
// 이건 33 일때만 적용 가능
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) {
logD("SDK=${Build.VERSION.SDK_INT}","")
requiredPermissions.plus(Manifest.permission.NEARBY_WIFI_DEVICES)
}
if (!hasPermissions(applicationContext, *requiredPermissions)) {
requestPermissions.launch(requiredPermissions)
}
// startLocalOnlyHotspot()
}

private fun startLocalOnlyHotspot() {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(
this,
Manifest.permission.NEARBY_WIFI_DEVICES
) != PackageManager.PERMISSION_GRANTED
) {
return
}
wifiManager.startLocalOnlyHotspot(
object : WifiManager.LocalOnlyHotspotCallback() {
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
super.onStarted(reservation)

}

override fun onFailed(reason: Int) {
super.onFailed(reason)
Toast.makeText(
applicationContext,
"Error Local Only Hotspot: $reason",
Toast.LENGTH_SHORT,
).show()
}

override fun onStopped() {
super.onStopped()

}
},
null,
)
}

private val requestPermissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()) { permissions ->

var chkE = false ;
permissions.entries.forEach {
logD("DEBUG", "${it.key} = ${it.value}")
if (!it.value) {
chkE = true
}
}
if (chkE) {
MaterialDialog(this).show {
icon(R.mipmap.ic_barcode_voucher_round)
title(R.string.titleGetPermission)
message(R.string.msgGetPermissonsNon)
positiveButton(R.string.OK) {
finish()
}
}
}
}

/**
* 장치를 광고 모드로 설정합니다. 검색 모드에서 다른 장치로 브로드캐스트합니다.
* [.onAdvertisingStarted] 또는 [.onAdvertisingFailed] 중 하나가 한 번 호출됩니다.
* 이 모드에 성공적으로 진입했는지 확인했습니다.
*/
protected fun startAdvertising() {
isAdvertising = true
val localEndpointName = name
val advertisingOptions = AdvertisingOptions.Builder()
advertisingOptions.setStrategy(strategy!!)
mConnectionsClient
.startAdvertising(
localEndpointName,
serviceId,
mConnectionLifecycleCallback,
advertisingOptions.build()
)
.addOnSuccessListener {
logV("Now advertising endpoint $localEndpointName")
onAdvertisingStarted()
}
.addOnFailureListener(
object : OnFailureListener {
override fun onFailure(e: Exception) {
isAdvertising = false
logW("startAdvertising() failed.", e)
onAdvertisingFailed()
}
})
}

/** Stops advertising. */
protected fun stopAdvertising() {
isAdvertising = false
mConnectionsClient.stopAdvertising()
}

/** Called when advertising successfully starts. Override this method to act on the event. */
protected fun onAdvertisingStarted() {}

/** Called when advertising fails to start. Override this method to act on the event. */
protected fun onAdvertisingFailed() {}

/**
* 원격 엔드포인트와의 보류 중인 연결이 생성될 때 호출됩니다. [연결 정보] 사용
* 연결에 대한 메타데이터(예: 수신 대 발신 또는 인증 토큰). 만약에
* 연결을 계속하려면 [.acceptConnection]을 호출합니다. 그렇지 않으면,
* [.rejectConnection]을 호출합니다.
*/
protected open fun onConnectionInitiated(
endpoint: Endpoint?,
connectionInfo: ConnectionInfo?
) {
}

/** Accepts a connection request. */
protected fun acceptConnection(endpoint: Endpoint) {
mConnectionsClient
.acceptConnection(endpoint.id, mPayloadCallback)
.addOnFailureListener { e -> logW("acceptConnection() failed.", e) }
}

/** Rejects a connection request. */
protected fun rejectConnection(endpoint: Endpoint) {
mConnectionsClient
.rejectConnection(endpoint.id)
.addOnFailureListener { e -> logW("rejectConnection() failed.", e) }
}

/**
* 장치를 검색 모드로 설정합니다. 이제 광고 모드에서 장치를 수신합니다. 어느 하나
* [.onDiscoveryStarted] 또는 [.onDiscoveryFailed]는 일단 찾으면 호출됩니다.
* 이 모드에 성공적으로 진입한 경우 출력됩니다.
*/
protected fun startDiscovering() {
isDiscovering = true
mDiscoveredEndpoints.clear()
val discoveryOptions = DiscoveryOptions.Builder()
discoveryOptions.setStrategy(strategy!!)
mConnectionsClient
.startDiscovery(
serviceId,
object : EndpointDiscoveryCallback() {
override fun onEndpointFound(endpointId: String, info: DiscoveredEndpointInfo) {
logD(
String.format(
"onEndpointFound(endpointId=%s, serviceId=%s, endpointName=%s)",
endpointId, info.serviceId, info.endpointName
),
""
)
if (serviceId == info.serviceId) {
val endpoint = Endpoint(endpointId, info.endpointName)
mDiscoveredEndpoints[endpointId] = endpoint
onEndpointDiscovered(endpoint)
}
}

override fun onEndpointLost(endpointId: String) {
logD(String.format("onEndpointLost(endpointId=%s)", endpointId), "")
}
},
discoveryOptions.build()
)
.addOnSuccessListener { onDiscoveryStarted() }
.addOnFailureListener(
object : OnFailureListener {
override fun onFailure(e: Exception) {
isDiscovering = false
logW("startDiscovering() failed.", e)
onDiscoveryFailed()
}
})
}

/** Stops discovery. */
protected fun stopDiscovering() {
isDiscovering = false
mConnectionsClient.stopDiscovery()
}

/** Called when discovery successfully starts. Override this method to act on the event. */
protected fun onDiscoveryStarted() {}

/** Called when discovery fails to start. Override this method to act on the event. */
protected fun onDiscoveryFailed() {}

/**
* Called when a remote endpoint is discovered. To connect to the device, call [ ][.connectToEndpoint].
*/
protected open fun onEndpointDiscovered(endpoint: Endpoint?) {}

/** Disconnects from the given endpoint. */
protected fun disconnect(endpoint: Endpoint) {
mConnectionsClient.disconnectFromEndpoint(endpoint.id)
mEstablishedConnections.remove(endpoint.id)
}

/** Disconnects from all currently connected endpoints. */
protected fun disconnectFromAllEndpoints() {
for (endpoint in mEstablishedConnections.values) {
mConnectionsClient.disconnectFromEndpoint(endpoint!!.id)
}
mEstablishedConnections.clear()
}

/** Resets and clears all state in Nearby Connections. */
protected fun stopAllEndpoints() {
mConnectionsClient.stopAllEndpoints()
isAdvertising = false
isDiscovering = false
isConnecting = false
mDiscoveredEndpoints.clear()
mPendingConnections.clear()
mEstablishedConnections.clear()
}

/**
* Sends a connection request to the endpoint. Either [.onConnectionInitiated] or [.onConnectionFailed] will be called once we've found out
* if we successfully reached the device.
*/
protected fun connectToEndpoint(endpoint: Endpoint) {
logV("Sending a connection request to endpoint $endpoint")
// Mark ourselves as connecting so we don't connect multiple times
isConnecting = true

// Ask to connect
mConnectionsClient
.requestConnection(name, endpoint.id, mConnectionLifecycleCallback)
.addOnFailureListener { e ->
logW("requestConnection() failed.", e)
isConnecting = false
onConnectionFailed(endpoint)
}
}

private fun connectedToEndpoint(endpoint: Endpoint?) {
logD(String.format("connectedToEndpoint(endpoint=%s)", endpoint), "")
mEstablishedConnections[endpoint!!.id] = endpoint
onEndpointConnected(endpoint)
}

private fun disconnectedFromEndpoint(endpoint: Endpoint?) {
logD(String.format("disconnectedFromEndpoint(endpoint=%s)", endpoint), "")
mEstablishedConnections.remove(endpoint!!.id)
onEndpointDisconnected(endpoint)
}

/**
* Called when a connection with this endpoint has failed. Override this method to act on the
* event.
*/
protected open fun onConnectionFailed(endpoint: Endpoint?) {}

/** Called when someone has connected to us. Override this method to act on the event. */
protected open fun onEndpointConnected(endpoint: Endpoint?) {}

/** Called when someone has disconnected. Override this method to act on the event. */
protected open fun onEndpointDisconnected(endpoint: Endpoint?) {}

/** Returns a list of currently connected endpoints. */
protected val discoveredEndpoints: Set<Endpoint>
get() = HashSet(mDiscoveredEndpoints.values)

/** Returns a list of currently connected endpoints. */
protected val connectedEndpoints: Set<Endpoint?>
get() = HashSet(mEstablishedConnections.values)

/**
* 현재 연결된 모든 엔드포인트에 [페이로드]를 보냅니다.
*
* @param payload 보내려는 데이터.
*/
protected fun send(payload: Payload) {
send(payload, mEstablishedConnections.keys)
}

fun send(payload: Payload, endpoints: Set<String>) {
mConnectionsClient
.sendPayload(ArrayList(endpoints), payload)
.addOnFailureListener { e -> logW("sendPayload() failed.", e) }
}

/**
* 우리와 연결된 누군가가 우리에게 데이터를 보냈습니다. 이벤트에 대해 작동하도록 이 메서드를 재정의합니다.
*
* @param endpoint 발신자.
* @param payload 데이터.
*/
protected open fun onReceive(endpoint: Endpoint?, payload: Payload?) {}

/** Returns the client's name. Visible to others when connecting. */
protected abstract val name: String

/**
* 서비스 ID를 반환합니다. 이것은 이 연결에 대한 작업을 나타냅니다. 발견할 때,
* 연결을 고려하기 전에 광고주가 동일한 서비스 ID를 가지고 있는지 확인합니다.
*/
protected abstract val serviceId: String

/**
* 다른 장치에 연결하는 데 사용하는 전략을 반환합니다. 동일한 전략을 사용하는 장치만
* 및 서비스 ID는 검색 시 나타납니다. Stragies는 들어오고 나가는 수를 결정합니다.
* 동시에 연결이 가능하며 사용 가능한 대역폭은 얼마입니까?
*/
protected abstract val strategy: Strategy?
@CallSuper
protected open fun logV(msg: String) {
Log.e(Constants.TAG, msg)
}

@CallSuper
protected open fun logD(msg: String, s: String) {
Log.e(Constants.TAG, String.format("%s %s", msg, s))
}

@CallSuper
protected open fun logW(msg: String) {
Log.e(Constants.TAG, msg)
}

@CallSuper
protected open fun logW(msg: String, e: Throwable) {
Log.e(Constants.TAG, msg, e)
}

@CallSuper
protected open fun logE(msg: String, e: Throwable) {
Log.e(Constants.TAG, msg, e)
}

protected open fun doSendNearBy(auth: FirebaseAuth) {}

protected open fun doAdvertSendNearBy() {}

/** Represents a device we can talk to. */
protected class Endpoint(val id: String, val name: String) {

override fun equals(other: Any?): Boolean {
if (other is Endpoint) {
return id == other.id
}
return false
}

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

override fun toString(): String {
return String.format("Endpoint{id=%s, name=%s}", id, name)
}
}

}

영문 코멘트는 번역기를 통해서 일부 번역을 해 보았습니다. 


 


오늘은 여기까지 기술해 보겠습니다. 다음 이야기는 테스트 중에 발견된 일부 사항을 수정한 다음 계속해 보겠습니다.


 


** 어제 확인이 미처 되지 못했던 부분에 대한 확인을 오늘에야 끝냈습니다.  일단, 권한 기술하는 부분에서 android 13에 맞게 기술이 되지 않았던 부분이 확인이 되었습니다.  2023.05.31 이전에 보셨다면 다시 확인해 보셔야 할 것 같습니다. 


 


아무튼 이렇게 기술해서 정리를 하고 나니 앱에서 상호 접속 하는 부분등이 정리가 되는 것을 확인할 수 있었습니다.


 


neayby 샘플 화면



 


연결이 되고 나면 오른쪽 상단에  개발자 페이지에 제공된 nearby 공식 이미지를 사이즈만 조금 줄여서 표시가 되도록 하였더니, 노출이 됩니다. 연결되었다는 메시지도 나오고요.


 


 





잠깐 동안의 영상을 통해서 접속이 잘 되는 지도 확인해 볼 수 있었습니다. 


 


이렇게 작성된 예제 코드는 아무튼 위에 있는 github을 참고해 보시도록 하시면 되겠습니다.    오늘도 즐 코딩 하세요.


 


 p.s : 한 가지 정리 중인 부분은 payload에 byte 문자열을 보내는 것을 테스트해보고 있는 데, 이미지를 byte 문자열로 해서 보내려고 했더니  아주 작은 사이즈 이미지 말고는 불가능해 보입니다.   그래서 다음번에는 file로 저장해서 전달하는 기능을 구현해 보고 정리를 해 보도록 하겠습니다.


 





오늘의 이야기

## 로또 번호 분석 및 추천 **데이터 분석 요약:** 총 20회차 데이터를 분석하여 각 회차별 번호 패턴을 파악하고, 이를 바탕으로 다음 회차 번호를 예측합니다. 분석 항목은 다음과 같습니다. 1. **연속 번호 간 간격:** 각 회차의 번호들 ...