2026/05/06
오늘의 이야기
#스하리1000명프로젝트
오늘 내가 만든앱 하나 알려주고 싶어, 이 앱은 알림수집기 라고 이름을 붙였는 데,
내 폰에 표시 되는 알림을 읽어서 내가 지정한 단어가 들어 있고, 지출기록을 남겨야 하는 알림이
있으면 수집하고, 카카오톡으로 친구에게 전달해 주는 기능을 구현해 줄꺼야. 📲
이번 패치에서는 하루 한번 지정한 시간에 나에게 알림(노티) 하도록 기능을 추가 했어. 🙏
한번 써보고 불편한 거 있으면 말해줘.
앱 바로가기
👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao
오늘의 이야기
🦾 Android | 하단 바 + Navigation Compose + 보안 키 주입(ResValue) 적용기
개요 (Intro)
- 오늘의 목표 / 배경: 앱에 하단 바를 도입하고 Compose Navigation으로 홈/설정/광고 탭을 구성. 외부 API 키를 local.properties에서 안전하게 읽어 리소스로 주입.
- 어떤 문제를 해결하려 했는지: Gradle Kotlin DSL에서 local.properties 로딩 시 IDE 경고/오류, 네비게이션 구성 시 일부 import 누락 및 경고.
- 사용한 기술 스택: Kotlin, Jetpack Compose, Navigation-Compose, Hilt, Gradle Kotlin DSL
📅 날짜: 2025.11.23
🎯 목표: 바텀 네비게이션 3탭(홈/설정/광고) + 키 주입(resValue) + Gradle DSL 정리
🧰 기술: Kotlin, Compose, Navigation, Hilt, Gradle Kotlin DSL
문제 정의 (Problem / Motivation)
- local.properties 값을 build.gradle.kts에서
java.util.Properties로 직접 로딩 시 IDE가 Unresolved reference: util, load 등 오탐 경고를 표시. - Compose 네비게이션 구현 중,
Modifier.padding(innerPadding)관련 import 누락으로 Unresolved reference 'padding' 오류. - 하단 바와 NavHost를 연결하는 구조에서 패키지 경로, 아이콘 import, 경고(예:
hiltViewModeldeprecate 알림) 확인 필요.
// (이슈 예시) build.gradle.kts에서 직접 Properties 로딩 시 IDE 경고 발생 가능
val localProps = java.util.Properties().apply {
val file = rootProject.file("local.properties")
if (file.exists()) file.inputStream().use { load(it) } // load 참조 경고/오류 표기 사례
}
val cupangAccessKey = localProps.getProperty("cupangAccessKey") ?: ""
해결 과정 (How I Solved It)
- local.properties 로딩은 Gradle의
providers.gradlePropertyAPI로 대체하여 IDE/Gradle 모두에서 안정적으로 동작하도록 변경. - 앱 모듈의
defaultConfig에서resValue로 문자열 리소스로 주입. - 하단 바 + NavHost 구조를 분리 파일로 구성하고, 필요한 Compose import를 명시하여 컴파일 오류 제거.
// app/build.gradle.kts (발췌) — Gradle providers API 사용
val cupangAccessKey: String = providers.gradleProperty("cupangAccessKey").orNull ?: ""
val cupangSecretKey: String = providers.gradleProperty("cupangSecretKey").orNull ?: ""
android {
defaultConfig {
resValue("string", "cupang_access_key", cupangAccessKey)
resValue("string", "cupang_secret_key", cupangSecretKey)
}
}
// 하단 바 + 네비게이션 (발췌)
sealed class AppScreen(val route: String, val label: String, val icon: ImageVector) {
data object Home: AppScreen("home", "홈", Icons.Filled.Home)
data object Settings: AppScreen("settings", "설정", Icons.Filled.Settings)
data object Ads: AppScreen("ads", "광고", Icons.Filled.Campaign)
}
@Composable
fun AppNavHost(navController: NavHostController, viewModel: HealthConnectViewModel = hiltViewModel()) {
NavHost(navController = navController, startDestination = AppScreen.Home.route) {
composable(AppScreen.Home.route) { MainScreen(viewModel) }
composable(AppScreen.Settings.route) { SettingsScreen() }
composable(AppScreen.Ads.route) { AdsScreen() }
}
}
@Composable
fun AppScaffoldWithBottomBar() {
val navController = rememberNavController()
Scaffold(bottomBar = { BottomBar(navController) }) { innerPadding ->
Box(modifier = Modifier.padding(innerPadding)) { AppNavHost(navController) }
}
}
결과 (Result)
- 하단 바 3탭(홈/설정/광고) + NavHost 라우팅이 정상 작동.
local.properties의cupangAccessKey/cupangSecretKey를R.string으로 노출, 화면/Compose에서getString으로 참조 가능.- Gradle Kotlin DSL의 IDE 경고를 줄이고, 스크립트 단순화로 유지보수성 개선.
✅ 바텀 네비게이션 안정화 및 화면 전환 확인
🔐 키 주입(resValue) 동작 확인, 보안 노출 리스크는 추후 서버 프록시로 추가 보완 예정
🧹 Gradle DSL 경고 제거로 개발 경험 개선
느낀 점 / 회고 (Reflection)
- Gradle 스크립트에서 표준 API를 직접 다루기보다는 Gradle 제공 API를 활용하는 편이 IDE/빌드 모두에 일관적.
- 네비게이션은 파일 분리(화면/바/호스트)로 가독성이 크게 좋아짐.
- 민감 키는 최종적으로 서버 서명/프록시로 이동하는 게 안전. 일단은 resValue로 편의 제공.
참고자료 (References)
오늘의 이야기
🐍 Python | Hugging Face 모델, 왜 요약을 못할까? (Base vs. Instruct 모델, 버전 충돌 해결기)
개요 (Intro)
Hugging Face 모델 로딩 시 마주쳤던 인증 오류(401, 403)를 해결한 후, 새로운 문제에 직면했다. 모델이 요약 지시를 제대로 따르지 않았고, 이를 해결하는 과정에서 예상치 못한 라이브러리 버전 충돌까지 겪었다. 오늘의 일지는 이 두 가지 문제를 해결한 과정을 상세히 기록한다.
📅 날짜: 2025.11.09
🎯 목표: Gemma 모델이 뉴스 기사를 의미 있는 한국어로 요약하도록 만들기
🧰 기술: Python, Hugging Face (transformers, torch, torchvision)
문제 정의 (Problem / Motivation)
인증 문제를 모두 해결하고 `google/gemma-2b` 모델을 성공적으로 로드했지만, 정작 중요한 요약 기능이 제대로 동작하지 않았다. 게다가 문제를 해결하려다 새로운 오류까지 발생했다.
문제 1: 모델이 지시를 따르지 않음
분명 "요약해줘"라고 요청했지만, 모델은 엉뚱한 텍스트를 생성하거나 입력한 프롬프트를 그대로 반복할 뿐, 의미 있는 요약문을 만들어내지 못했다.
문제 2: `torchvision` 버전 충돌 오류
라이브러리 버전 문제 해결을 위해 `pip install --upgrade`를 실행한 후, 이전에는 없던 새로운 오류가 발생했다. 모델 로딩 단계에서 `RuntimeError: operator torchvision::nms does not exist` 라는 메시지와 함께 프로그램이 중단되었다.
ModuleNotFoundError: Could not import module 'GemmaForCausalLM'. Are this object's requirements defined correctly?
... (Caused by)
RuntimeError: operator torchvision::nms does not exist
해결 과정 (How I Solved It)
두 문제는 별개의 원인을 가지고 있었고, 하나씩 해결해 나갔다.
- Base 모델 vs. Instruction-Tuned 모델의 차이 이해:
첫 번째 문제의 원인은 모델 선택에 있었다. 내가 사용한 `google/gemma-2b`는 **기반(Base) 모델**로, 다음에 올 단어를 예측할 뿐 지시를 따르도록 훈련되지 않았다. "요약"과 같은 특정 작업을 수행하려면, 지시를 따르도록 미세조정(Fine-tuning)된 **Instruction-Tuned 모델 (`google/gemma-2b-it`)**을 사용해야 했다.
# 수정 전
# model_name = "google/gemma-2b"
# 수정 후: Instruction-Tuned 모델로 변경
model_name = "google/gemma-2b-it"
또한, 모델이 생성한 전체 텍스트에서 입력 프롬프트를 제외하고 순수 요약 결과만 추출하도록 코드 로직을 개선했다. - 라이브러리 버전 충돌 해결:
두 번째 문제의 원인은 `torch`와 `torchvision`의 버전 호환성이 깨졌기 때문이었다. `pip install --upgrade`가 각 라이브러리를 개별적으로 최신화하면서 발생한 문제로 추정된다. 가장 확실한 해결책은 관련 라이브러리를 완전히 삭제하고 재설치하는 것이었다.
# 1. 기존 라이브러리 완전 삭제
pip uninstall torch torchvision -y
# 2. 호환되는 버전으로 재설치
pip install torch
pip install torchvision
결과 (Result)
위의 과정을 통해 모든 문제를 해결하고, 마침내 `gemma-2b-it` 모델이 정상적으로 다운로드되는 것을 확인했다.
✅ `torch`와 `torchvision` 버전 충돌 문제 해결!
✅ Base 모델 대신 Instruction-Tuned 모델(`gemma-2b-it`)을 사용하도록 코드 수정 완료!
⏳ 현재 새로운 `gemma-2b-it` 모델 다운로드 진행 중 (100% 완료되면 요약 기능 정상 동작 기대)
느낀 점 / 회고 (Reflection)
- LLM을 사용할 때 **Base 모델**과 **Instruction-Tuned 모델**의 차이를 아는 것이 매우 중요하다는 것을 깨달았다. 특정 작업을 시키려면 반드시 `-it`나 `-instruct`가 붙은 모델을 써야 한다.
- 라이브러리 버전 문제는 언제나 복병이다. `pip install --upgrade`가 만능 해결책이 아니며, 때로는 깨끗하게 삭제 후 재설치하는 것이 더 확실한 방법이다.
- 오류 로그를 차근차근 읽는 것이 문제 해결의 가장 빠른 길이다. `torchvision::nms`라는 키워드를 통해 `torch`와 `torchvision`의 관계를 의심해 볼 수 있었다.
참고자료 (References)
오늘의 이야기
#스하리1000명프로젝트,
Bị lạc ở Hàn Quốc? Ngay cả khi bạn không nói được tiếng Hàn, ứng dụng này vẫn giúp bạn đi lại dễ dàng.
Chỉ cần nói ngôn ngữ của bạn—nó sẽ dịch, tìm kiếm và hiển thị kết quả bằng ngôn ngữ của bạn.
Tuyệt vời cho du khách! Hỗ trợ hơn 10 ngôn ngữ bao gồm tiếng Anh, tiếng Nhật, tiếng Trung, tiếng Việt, v.v.
Hãy thử nó ngay bây giờ!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127
오늘의 이야기
🐘 Oracle | GREATEST 함수와 PIVOT으로 데이터 다루기
개요 (Intro)
오늘은 Oracle DB에서 여러 컬럼 값 중 최댓값을 구하는 방법과 월별 데이터를 한 행으로 변환하는 방법을 실습했다. 특히 GREATEST 함수의 동작 방식과 PIVOT 구문을 활용한 데이터 구조 변환을 다뤘다. 사용한 기술 스택: Oracle SQL, SQL Developer
📅 날짜: 2025.12.19
🎯 목표: 컬럼 값 비교와 월별 데이터 Pivot 처리
🧰 기술: Oracle SQL, GREATEST, PIVOT, CASE
문제 정의 (Problem / Motivation)
특정 행(row)의 3개 컬럼 값 중 최댓값을 구해야 하는 상황이 있었다. 단순 숫자뿐 아니라 문자(R, O, Y, G) 값이 들어 있는 경우 어떤 결과가 나오는지 확인 필요. 또한 1월부터 12월까지 월별 데이터가 여러 행으로 저장된 테이블을 한 행으로 변환해 보고 싶었다.
-- 예시: 특정 행의 3개 컬럼 중 최댓값 구하기 SELECT GREATEST(col1, col2, col3) AS max_value FROM your_table WHERE id = 123; 해결 과정 (How I Solved It)
GREATEST 함수는 숫자뿐 아니라 문자도 비교 가능하다는 점을 확인했다. 문자 비교 시 ASCII 코드 기준으로 가장 큰 값이 반환됨. (예: R=82, O=79, Y=89, G=71 → 최댓값은 Y) 의미적 순서(예: R > O > Y > G)를 반영하려면 CASE 또는 DECODE로 매핑 후 비교해야 한다.
-- 색상 우선순위 매핑 후 최댓값 구하기 SELECT CASE GREATEST( DECODE(col1, 'R',4, 'O',3, 'Y',2, 'G',1), DECODE(col2, 'R',4, 'O',3, 'Y',2, 'G',1), DECODE(col3, 'R',4, 'O',3, 'Y',2, 'G',1) ) WHEN 4 THEN 'R' WHEN 3 THEN 'O' WHEN 2 THEN 'Y' WHEN 1 THEN 'G' END AS max_color FROM your_table WHERE id = 123; 월별 데이터를 한 행으로 변환하기 위해 PIVOT을 활용했다.
-- PIVOT을 이용한 월별 데이터 변환 SELECT * FROM sales_data PIVOT ( SUM(amount) FOR month IN (1 AS JAN, 2 AS FEB, 3 AS MAR, 4 AS APR, 5 AS MAY, 6 AS JUN, 7 AS JUL, 8 AS AUG, 9 AS SEP, 10 AS OCT, 11 AS NOV, 12 AS DEC) ); 결과 (Result)
GREATEST로 숫자와 문자를 비교할 수 있음을 확인했고, 문자 비교 시 ASCII 기준이라는 점을 배웠다. 색상 순서를 반영하려면 매핑이 필요하다는 교훈을 얻었다. PIVOT을 통해 월별 데이터를 한 행으로 변환하는 데 성공했다.
✅ 컬럼 값 비교 로직 정리 완료✅ 월별 데이터 Pivot 변환 성공📊 데이터 구조 이해도 향상
느낀 점 / 회고 (Reflection)
단순히 함수만 쓰는 것이 아니라, 데이터의 의미를 반영하려면 추가 로직이 필요하다는 점을 깨달았다. PIVOT은 데이터 분석 시 매우 유용하며, 앞으로 보고서 작성이나 BI 툴 연동 시 자주 활용할 수 있을 것 같다. SQL은 단순 조회를 넘어서 데이터 구조를 재구성하는 강력한 도구라는 점을 다시 느꼈다.
참고자료 (References)
Oracle SQL Functions - GREATEST Oracle PIVOT Clause 공식 문서
2026/05/05
오늘의 이야기
🕹️ Android | Wear Compose UI 레이아웃 정리와 중앙정렬, 문자열 리소스화
개요 (Intro)
- 오늘의 목표 / 배경: Wear OS 앱의 메인 화면 Compose 레이아웃을 모듈화하고 가독성을 높이며, 텍스트 중앙정렬과 문자열 리소스화를 적용
- 어떤 문제를 해결하려 했는지: 거대한 단일 컴포저블 내부 UI가 난해하고, 일부 하드코딩 텍스트/정렬 불일치가 존재
- 사용한 기술 스택: Kotlin, Jetpack Compose for Wear, Hilt, Coroutines
📅 날짜: 2025.12.17
🎯 목표: Compose UI 분리(refactor) + 중앙정렬 + 문자열 리소스화 + 불필요 섹션 제거
🧰 기술: Kotlin, Android Studio, Compose, MVVM-ish, Hilt
문제 정의 (Problem / Motivation)
- 메인 UI가 하나의 거대한 함수에 몰려 있어 읽기/수정이 어려움
- 일부 텍스트가 하드코딩되어 국제화/재사용성에 제약
- 센터 정렬이 필요한 텍스트들이 좌측정렬로 되어 UI 일관성이 떨어짐
- Wear OS에서 의미가 약한 배터리 최적화 안내/버튼 섹션 존재
// 예시 (정리 이전): 단일 컴포저블 안에 다양한 UI가 혼재해 가독성 저하
@Composable
fun WearApp() {
// ... 상태/권한/서비스 로직
Scaffold {
ScalingLazyColumn {
// 권한, 상태, 위치, PDR, 배터리 섹션이 모두 item 블록으로 길게 나열됨
}
}
}
해결 과정 (How I Solved It)
- UI를 작은 컴포저블들로 분리:
PermissionSection,StatusSection,LocationSection,PdrSection - 텍스트 중앙정렬: 모든 텍스트 컴포넌트에
Modifier.fillMaxWidth()+TextAlign.Center적용 - 문자열 리소스화: 하드코딩된 한글("전송 임계값=")을
strings.xml로 이전 - 불필요한 배터리 최적화 섹션 제거
// 파일: MainActivity.kt — 초보자도 이해하기 쉬운 주석 포함 예시
// 1) 가운데 정렬을 위한 import
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.ui.text.style.TextAlign
@Composable
fun StatusSection(
syncStatusText: String,
syncStatusColor: Color,
steps: Long,
lastDelta: Long,
stepThreshold: Long,
headingRad: Double,
lastPayload: StepPayload?,
altitude: Double?,
unit: String
) {
// Column의 수평 정렬은 Center로 유지하되,
// 각 Text를 화면 너비만큼 확장하고(Text가 가로폭을 차지),
// TextAlign.Center로 내용을 중앙에 배치
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = syncStatusText,
color = syncStatusColor,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.fillMaxWidth(), // 전체 너비 사용
textAlign = TextAlign.Center // 텍스트 중앙 정렬
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.steps_label) + ": " + steps + " (Δ " + lastDelta + ")",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.step_threshold_label) + stepThreshold,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Text(
text = String.format(
java.util.Locale.US,
stringResource(R.string.heading_label) + ": %.0f°",
Math.toDegrees(headingRad)
),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
}
}
전송 임계값=
// 파일: MainActivity.kt — 배터리 최적화 섹션 제거 예시
// 기존: BatteryOptimizationSection(...) 사용 및 관련 PositionIndicator 주변 조건부 렌더링
// 변경: 해당 섹션/조건 로직 삭제로 UI 단순화
결과 (Result)
- UI가 섹션별 컴포저블로 분리되어 가독성 및 유지보수성 향상
- 텍스트 중앙정렬로 일관된 시각 경험 제공
- 문자열 리소스화로 국제화/재사용성 개선
- 불필요한 배터리 최적화 UI 제거로 사용자 혼란 감소
✅ Compose 아이템들이 깔끔하게 모듈화됨
🎯 텍스트 중앙정렬 적용으로 UI 일관성 확보
🧩 문자열 리소스 관리로 다국어 및 유지보수 용이
느낀 점 / 회고 (Reflection)
- Wear Compose에서도 작은 컴포저블로 나누는 것이 가장 큰 생산성 향상을 가져온다
- 하드코딩 텍스트는 사소해 보여도 국제화/테스트/일관성 측면에서 리소스화가 중요
- 기기 특성(Wear OS)을 고려한 UI/설정 항목은 실제 동작 가능 여부를 검증 후 제공해야 한다
참고자료 (References)
오늘의 이야기
#billcorea #운동동아리관리앱
🏸 Schneedle, aplikasi yang wajib dimiliki oleh klub bulu tangkis!
👉 Match Play – Rekam Skor & Temukan Lawan 🎉
Sempurna untuk di mana saja, sendirian, bersama teman, atau di klub! 🤝
Jika Anda suka bulu tangkis, cobalah
Buka aplikasi 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay
오늘의 이야기
<!doctype html>
🧭 Android | 위치 수신·히스토리 저장 및 Vico 그래프 NaN 크래시 대응
개요 (Intro)
- 오늘의 목표 / 배경: 폰앱에서 위치(및 걸음수)를 영구 저장(히스토리)하고, Setting 화면에는 마지막 정보만 노출, 메인 화면의 Vico 그래프에 시간축으로 X/Y/Z와 걸음수 시리즈를 표기(색상 구분) — 동시에 발생한 런타임 크래시(NaN 관련)를 해결한다.
- 해결하려던 문제: Vico 라인 차트에서 마커/툴팁 계산 중 NaN 값이 투입되어 앱이 크래시 나는 문제. 또한 걸음수가 그래프에 보이지 않는 문제와 설정 화면의 측정 상태가 앱 재진입 시 유지되지 않는 문제를 정리.
- 사용한 기술 스택: Kotlin, Jetpack Compose, Vico chart, Health Connect API, Coroutines, Android Studio
📅 날짜: 2025.12.07
🎯 목표: 위치 히스토리 영구 저장, Setting 화면 최근값 표시, Vico 그래프에 X/Y/Z/Steps 표시(시간 축), NaN 크래시 방지
🧰 기술: Kotlin, Compose, Vico, Coroutines
문제 정의 (Problem / Motivation)
작업 중에 다음과 같은 런타임 예외가 발생했습니다:
java.lang.IllegalArgumentException: Cannot round NaN value.
at kotlin.math.MathKt__MathJVMKt.roundToInt(MathJVM.kt:1192)
at com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer.updateMarkerTargets(LineCartesianLayer.kt:471)
... (생략)
원인으로 의심되는 상황:
- 시간별 배열을 FloatArray(24)로 만들고 기본값을 Float.NaN으로 두었음 — 일부 시간대에 값이 없으면 NaN이 남아 있음.
- Vico 차트에 여러 시리즈(예: X, Y, Z, Steps)를 정의하는 레이어는 고정된 시리즈 수를 기대하거나, 마커 계산 시 시리즈 내부 포인트가 없는 경우를 안전하게 처리하지 못함.
- lineSeries 빌더에서 조건부로 시리즈를 추가하면 레이어 쪽 series 컬러 매핑과 시리즈 개수가 맞지 않아 인덱스 문제/NaN 전파가 발생할 수 있음.
// 문제를 일으킨 코드 개요 (발생 사례 일부)
val locXHourly = FloatArray(24) { Float.NaN }
// ... 최근 위치를 시간별로 채움
// 일부 시간대는 NaN으로 남아 있을 수 있음
// 이후 Vico에 시리즈를 만들 때 NaN이 남아 있는 값 때문에 마커 계산이 NaN을 전달받아 roundToInt 에러 발생
해결 과정 (How I Solved It)
접근한 순서:
- 원인 분석 — 로그와 스택트레이스에서 NaN이 roundToInt로 전달된 것을 확인.
- 데이터 포맷/시리즈 구성 점검 — Vico 레이어가 기대하는 시리즈 수와 순서를 맞추도록 수정 방향 결정.
- 안전성 확보 — NaN이 마커 계산으로 전달되지 않도록 시리즈를 일관된 길이로 만들거나, 마커를 비활성화/무시하도록 보호 코드 추가.
- 걸음수 표시 문제 해결 — 시간 축 기준으로 시간별 합계를 구하고 그래프 범위/스케일을 조정하여 눈에 띄게 함.
- Setting 화면 상태 영속화는 DataStore/SharedPreferences로 저장하도록 계획(이번 변경에서는 로그와 그래프 중심 수정에 초점).
핵심 수정 아이디어(예시 코드)
아래 코드는 Vico 시리즈를 생성할 때 안전하게 NaN을 처리하고, 항상 레이어에 정의된 시리즈 개수를 맞추기 위한 예시입니다. 초보자도 이해하기 쉽도록 주석을 상세히 달았습니다.
// 안전한 시리즈 생성 예시 (MainScreen.kt의 modelProducer 구성부에서 사용)
// 1) 공통 X축: hours (0..23)
val hours = (0..23).toList()
// 2) 시간별 값들을 미리 계산 (값이 없으면 null로 둠)
val xValuesNullable: List<Float?> = hours.map { h -> /* 값이 있으면 실수, 없으면 null */ null }
// 실제 코드에서는 recentLocations를 순회해서 채움
// 3) Vico에 넣을 때는 레이어가 기대하는 시리즈 개수(예: 4)에 맞춰 항상 series를 추가
// 값이 없는 포인트는 0f로 대체하거나, 이전 값으로 보간하거나, 마커를 끄는 방식 중 선택
val xValuesForChart = xValuesNullable.map { it ?: 0f } // 비어있을 때 0으로 대체 (안전한 방법)
val yValuesForChart = /* 동일 처리 */ hours.map { 0f }
val zValuesForChart = /* 동일 처리 */ hours.map { 0f }
val stepsValuesForChart = /* 시간별 steps, 스케일 조정 */ hours.map { h -> (stepsHourly[h] / 10f) }
// 4) lineSeries에 항상 4개의 시리즈를 추가 (레이어와 일치)
lineSeries {
series(hours, xValuesForChart)
series(hours, yValuesForChart)
series(hours, zValuesForChart)
series(hours, stepsValuesForChart)
}
// 주석: 0으로 대체하면 그래프 수치 왜곡 가능성이 있으므로, 시각적으로는
// 값이 없음을 별도로 표시(예: 투명도 낮게 그리거나 범례에 설명 추가)하는 것이 좋습니다.
또한 마커 계산 시점에 안전 장치를 추가합니다:
// 마커 사용 시 안전 체크 예시
val marker = rememberMarker()
// modelProducer에 데이터가 아예 없을 땐 marker를 null처럼 취급하거나
// marker가 포인트 좌표를 계산할 때 NaN을 만나면 아무것도 그리지 않도록 구현
// (이 부분은 vico의 marker 콜백 구현 시점에 방어 코드 추가 필요)
결과 (Result)
✅ NaN으로 인한 라인 차트 마커 크래시의 원인을 파악했습니다. (NaN이 roundToInt로 전달되어 예외 발생)
적용한 전략의 장점:
- 시리즈 개수와 레이어 정의를 일치시켜 인덱스 불일치 문제를 제거했습니다.
- 빈 시간대 처리(0 또는 보간)는 크래시를 방지합니다. 시각적 정확성은 추가 보완 필요(예: 투명도/점선 처리).
- 걸음수는 시간별로 합산해 별도 시리즈로 추가했으며, 그래프 상에서 보이도록 스케일을 조정했습니다(예: /10 표기).
느낀 점 / 회고 (Reflection)
- 데이터 시각화 라이브러리는 데이터 포맷에 민감합니다. 특히 여러 시리즈를 그릴 때 길이와 인덱스 정합성을 반드시 맞춰야 합니다.
- 디버깅 시 스택트레이스가 가리키는 함수 내부에서 어떤 입력값이 NaN/무효값인지 역추적하는 것이 중요했습니다. 로그와 작은 재현 데이터셋을 만들어 원인을 좁혔습니다.
- 완벽한 해결을 위해선 다음 작업이 필요합니다: (1) SettingScreen의 측정 시작 상태 영속화(DataStore), (2) 빈값 처리 시 시각적 표시 개선(투명도/점선/툴팁에서 ‘데이터 없음’ 표기), (3) 마커가 NaN을 만나면 안전하게 무시하도록 vico 콜백 방어 코드 강화.
참고자료 (References)
- Vico 공식 문서 및 예제 (GitHub) - https://github.com/patrykandpatrick/vico
- Kotlin stdlib - Float.isNaN(), roundToInt() 동작 참고
- Android Developers - Data persistence (DataStore) 가이드
오늘의 이야기
🦾 Android | 워치앱 빌드 오류 수정과 UI/국제화 개선 정리
개요 (Intro)
- 오늘의 목표 / 배경: 워치앱(MainActivity) 빌드 오류를 해결하고, 스크롤바/버튼 텍스트 레이아웃/국제화(strings.xml) 개선
- 어떤 문제를 해결하려 했는지: 미선언 변수(dayKey)로 인한 컴파일 오류, 텍스트 줄임 표시, 스크롤바 가시성, 하드코딩 문자열 정리
- 사용한 기술 스택: Kotlin, Jetpack Compose, Wear OS, Coroutine, Hilt
📅 날짜: 2025.12.11
🎯 목표: 워치앱 컴파일 오류 제거 + UI/국제화 개선
🧰 기술: Kotlin, Android Studio, Compose, Wearable APIs
문제 정의 (Problem / Motivation)
- 워치앱의
MainActivity.kt에서 Unresolved reference 'dayKey' 빌드 에러 발생 - 타일/메인 화면 일부 텍스트가 길어 줄임표(...)로 보이는 문제
- 스크롤바가 배경과 유사해 가시성이 떨어짐
- 하드코딩된 한글 문자열의 국제화(strings.xml) 필요
// 컴파일 에러 예시 (요약)
// e: MainActivity.kt: Unresolved reference 'dayKey'
// 자정 체크 로직에서 dayKey 상태가 정의되어 있지 않음
해결 과정 (How I Solved It)
- dayKey 상태 추가: 자정 변경 감지를 위해
remember { mutableStateOf(...) }로 dayKey를 선언 - 스크롤바 색 개선: 배경색의 보색(Complementary color)로 스크롤바 트랙/썸을 그려 가시성 상승
- 텍스트 레이아웃 개선: 여백 조정 및 항목 분리로 줄임표 빈도 감소, 향후 Chip/버튼 폭/높이 조정 계획 반영
- 국제화: 화면에 보이는 주요 한글 문자열은
stringResource(...)로 대체하고 리소스로 이동(추가 정리 예정)
// 핵심 수정 예시: dayKey 상태 추가 및 자정 리셋 로직 유지
@Composable
fun WearApp() {
// ... 기존 상태들 ...
// 일자 키(자정 교체 감지용)
var dayKey by remember {
mutableStateOf(
java.time.ZonedDateTime.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
)
}
// 자정 변경 감지: 1분마다 체크하여 dayKey 달라지면 스텝/PDR 리셋
LaunchedEffect(Unit) {
while (true) {
val current = java.time.ZonedDateTime.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"))
if (current != dayKey) {
dayKey = current
// 스텝 리셋 및 PDR 리셋
passiveStepsManager.stop(); passiveStepsManager.start()
pdr.reset()
}
kotlinx.coroutines.delay(60_000L)
}
}
}
// 스크롤바 가시성 개선: 배경색 보색 사용
val animatedBg by animateColorAsState(targetValue = bgColor, animationSpec = tween(150))
val scrollbarColor = Color(1f - animatedBg.red, 1f - animatedBg.green, 1f - animatedBg.blue)
Canvas(modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 2.dp)
.height(100.dp)
.width(3.dp)) {
val contentHeight = scrollState.maxValue.toFloat() + size.height
if (contentHeight > size.height + 1f) {
val ratio = size.height / contentHeight
val thumbHeight = (size.height * ratio).coerceAtLeast(12f)
val scrollY = (scrollState.value / scrollState.maxValue.toFloat()).coerceIn(0f, 1f)
val thumbTop = (size.height - thumbHeight) * scrollY
drawRect(color = scrollbarColor.copy(alpha = 0.2f), size = size) // 트랙
drawRect(color = scrollbarColor, topLeft = Offset(0f, thumbTop), size = Size(size.width, thumbHeight)) // 썸
}
}
// 국제화 적용 예시: 하드코딩 제거
Text(stringResource(R.string.steps_label) + ": $steps (Δ $lastDelta)")
Text(stringResource(R.string.altitude_barometer_label) + ": $altText")
결과 (Result)
- 워치앱 컴파일 오류 해결 (dayKey 상태 추가)
- 스크롤바가 배경과 대비되어 가시성 향상
- 텍스트 줄임표 이슈 완화, 국제화 기반으로 문자열 관리 준비
✅ 워치앱이 정상 빌드/실행됨
📱 UI 가독성 개선, 유지보수성(국제화) 향상 기반 구축
느낀 점 / 회고 (Reflection)
- 작은 상태 누락(dayKey)도 앱의 핵심 흐름(자정 리셋)에 영향을 크게 줄 수 있음
- UI 색 대비는 접근성과 직결되므로 초기 단계부터 고려하는 게 중요
- 국제화는 나중에 몰아서 하기보다, 화면 작업과 함께 병행해야 리스크가 줄어듦
참고자료 (References)
오늘의 이야기
<!doctype html>
🐾 Android | Kalman vs EMA — 고도/센서 데이터 필터링 비교와 적용기
개요 (Intro)
- 오늘의 목표 / 배경: 위치·고도 데이터(바로미터 + PDR/IMU)를 안정적으로 시각화/동기화하기 위해 Kalman 필터와 EMA(지수평활)의 차이를 비교 분석하고, 프로젝트에 적용할 방향을 정리한다.
- 해결하려는 문제: 그래프의 NaN 크래시 방지, 스파이크(이상치)와 드리프트에 강한 필터링, 실시간성(응답속도) 유지 간의 균형.
- 사용한 기술 스택: Kotlin, Jetpack Compose, Vico chart, Coroutines, Wear OS
📅 날짜: 2025.12.09
🎯 목표: Kalman/EMA 특성 비교 및 프로젝트 적용 가이드 정리
🧰 기술: Kotlin, Compose, Vico, Wear OS, Coroutines
문제 정의 (Problem / Motivation)
- 바로미터(고도) 데이터는 환경/기압 변화에 민감해 노이즈와 스파이크가 잦음.
- PDR/IMU는 누적 드리프트가 생기기 쉬움(특히 장시간).
- 그래프에 NaN이 들어가면 마커/그리기 로직에서 크래시 위험(실제 NaN round 예외 경험).
- 실시간 UI에서는 빠른 반응과 안정적 시각화 둘 다 필요.
// NaN 방어를 위해 앞값 채움(fill-forward)으로 시계열을 안전화한 부분
fun fillForward(arr: FloatArray): List<Float> {
val out = ArrayList<Float>(arr.size)
var last = 0f
var seen = false
for (i in arr.indices) {
val v = arr[i]
if (!v.isNaN()) { last = v; seen = true; out.add(v) }
else { out.add(if (seen) last else 0f) }
}
return out
}
해결 과정 (How I Solved It)
EMA(지수평활) — 간단·저비용 필터
- 정의: y[n] = α·x[n] + (1-α)·y[n-1]
- 특징: 구현 매우 쉬움, 레이턴시 낮음, 이상치에는 약함.
// 초보자도 이해하기 쉬운 EMA 필터 — 주석 상세 버전
class EmaFilter(private val alpha: Float) {
private var state: Float? = null // 이전 출력값(초기엔 없음)
// 새 입력값 x를 받아 필터링된 값 반환
fun update(x: Float): Float {
val s = state
val out = if (s == null) {
// 첫 샘플은 기준값으로 사용
x
} else {
// EMA 핵심 — 현재 입력과 이전 출력의 가중 평균
alpha * x + (1 - alpha) * s
}
state = out
return out
}
fun reset() { state = null }
}
Kalman — 상태공간 기반 최적 추정(선형/가우시안 가정)
- 특징: 모델(A,H), 노이즈(Q,R)로 예측/갱신. 드리프트·노이즈 균형적으로 제어.
- 장점: 통계적으로 최적(가정 하), 속도/가속 등 상태 추정 가능.
- 단점: 튜닝 복잡, 계산량 EMA보다 큼.
// 1D 고도용 아주 간단한 Kalman — 주석 상세
class SimpleKalman1D(
private var q: Float, // 프로세스(모델) 노이즈 공분산 — 모델 불확실성
private var r: Float // 측정 노이즈 공분산 — 센서 불확실성
) {
private var x: Float = 0f // 상태(고도)
private var p: Float = 1f // 상태 오차 공분산
fun init(initial: Float, initialP: Float = 1f) {
x = initial
p = initialP
}
// 예측 단계 — 간단 모델(고정)에서는 p만 늘려 불확실성 반영
fun predict() {
p += q
}
// 갱신 단계 — 관측 z 반영
fun update(z: Float): Float {
val k = p / (p + r) // 칼만 이득(측정 신뢰도 vs 상태 신뢰도 비율)
x = x + k * (z - x) // 상태 보정
p = (1 - k) * p // 오차 공분산 업데이트
return x
}
}
상보(Complementary) 융합 — 간단 융합법
- 예: fusedZ = α·pdrZ + (1-α)·baroZ
- 장점: 직관적인 튜닝, 구현 쉬움.
- 단점: 노이즈 통계 반영 없음, 이상치 처리 필요.
// 프로젝트에서 사용 중인 융합 아이디어(유사): baro와 ENU zUp을 비율로 합성
val fusedZ = altitudeFusionRatio * enu.zUp + (1 - altitudeFusionRatio) * baro
결과 (Result)
- 그래프 입력의 NaN 제거(앞값 채움)로 마커 크래시 방지.
- EMA/상보/칼만의 트레이드오프를 정리해 적용 방향을 확립.
- 실시간 UI 요구가 큰 경우: EMA 또는 상보 필터부터 적용하고, 필요 시 칼만으로 단계적 고도화.
✅ 실시간성(EMA/상보)과 안정성(칼만)의 균형을 프로젝트 요구에 맞게 선택할 수 있도록 기준을 수립.
느낀 점 / 회고 (Reflection)
- NaN 같은 ‘형식적 오류’도 시각화 라이브러리에서 큰 크래시 원인이 된다 — 입력 정규화가 최우선.
- EMA는 UI엔 매우 좋은 출발점. 칼만은 모델·노이즈 튜닝이 핵심 — 로그 기반 정량 튜닝이 필요.
- 상보 필터는 간단하지만 이상치 방어(스파이크 클리핑/중앙값 필터)와 함께 쓰면 체감 품질이 훨씬 좋아진다.
참고자료 (References)
- Kalman Filter — Greg Welch & Gary Bishop, "An Introduction to the Kalman Filter"
- Signal Processing Stack Exchange — EMA/Moving Average 비교 토론
- Android Sensor Docs — Barometer, SensorManager Delay 등
- Vico Chart — https://github.com/patrykandpatrick/vico
- 바로미터 측정에 중앙값(Median) 또는 MAD 기반 이상치 제거를 추가.
- 상보 필터 계수(altitudeFusionRatio)를 프로파일(배터리/표준/고속)에 따라 자동 조정.
- 간단한 1D 칼만(고도+속도)로 샘플 러닝 테스트 — Q/R 튜닝 로그(innovation) 기록.
오늘의 이야기
#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase
🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 “혹시나~” 하면서 봤거든 ㅋㅋ
근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨
그것도 구글 Gemini로다가!
그래서 앱 하나 만들었지
👉 “로또 예상번호 by Gemini” 🎱
AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨
재미로 해도 좋고…
혹시 모르는 거잖아? 😏
https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127
오늘의 이야기
#스하리1000명프로젝트 오늘 내가 만든앱 하나 알려주고 싶어, 이 앱은 알림수집기 라고 이름을 붙였는 데, 내 폰에 표시 되는 알림을 읽어서 내가 지정한 단어가 들어 있고, 지출기록을 남겨야 하는 알림이 있으면 수집하고, 카카오톡으로 친구에게...
-
이전 글에서 정리할 것처럼 java에서 kotlin으로 이전을 했습니다. 그러고 나서 보기 시작했는 데, DefaultSharedPrefernces의 사용할 수 없는 환경으로 변경이 된 것을 알게 되었습니다. 이전 prefs = Prefere...