billcorea.com
빌코리아의 홈페이지 입니다.
2026/05/20
오늘의 이야기
#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase
🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 “혹시나~” 하면서 봤거든 ㅋㅋ
근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨
그것도 구글 Gemini로다가!
그래서 앱 하나 만들었지
👉 “로또 예상번호 by Gemini” 🎱
AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨
재미로 해도 좋고…
혹시 모르는 거잖아? 😏
https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127
오늘의 이야기
#스하리1000명프로젝트
오늘 내가 만든앱 하나 알려주고 싶어, 이 앱은 알림수집기 라고 이름을 붙였는 데,
내 폰에 표시 되는 알림을 읽어서 내가 지정한 단어가 들어 있고, 지출기록을 남겨야 하는 알림이
있으면 수집하고, 카카오톡으로 친구에게 전달해 주는 기능을 구현해 줄꺼야. 📲
이번 패치에서는 하루 한번 지정한 시간에 나에게 알림(노티) 하도록 기능을 추가 했어. 🙏
한번 써보고 불편한 거 있으면 말해줘.
앱 바로가기
👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao
오늘의 이야기
#스하리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
오늘의 이야기
#billcorea #운동동아리관리앱
🏸 Schneedle, một ứng dụng cần có cho các câu lạc bộ cầu lông!
👉 Đấu trận – Ghi điểm & Tìm đối thủ 🎉
Hoàn hảo cho mọi nơi, một mình, với bạn bè hoặc trong câu lạc bộ! 🤝
Nếu bạn thích cầu lông, nhất định phải thử nó
Vào ứng dụng 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay
오늘의 이야기
Python에서 5100개 키워드 포함 여부 빠르게 검사하기
대규모 로그 처리나 텍스트 분석을 하다 보면, 한 문장에 수천 개의 키워드 중 하나라도 포함되어 있는지 빠르게 확인해야 할 때가 있습니다. 단순히 re.search를 5100번 반복하는 방식은 성능이 매우 떨어지므로, 더 효율적인 방법을 소개합니다.
❌ 잘못된 접근: [...] 문자 클래스
정규식에서 [...]는 문자 클래스로 동작합니다.
예: [abc] → "a" 또는 "b" 또는 "c"라는 단일 문자 매치.
따라서 5100개의 키워드를 [...] 안에 넣는 것은 "5100개의 문자열 중 하나"가 아니라 "5100개의 문자 중 하나"를 찾는 것에 불과합니다.
✅ 올바른 접근 방법
1. Set 기반 검색
# 5100개 키워드 준비
keywords = ["error", "warning", "critical", "timeout", "failed", ...]
keywords_set = {k.lower() for k in keywords} # 모두 소문자로 변환
sentence = "System reported CRITICAL failure at 12:00"
# 방법 1: 단어 단위 검색
words = set(sentence.lower().split())
if words & keywords_set:
print("포함됨 (단어 단위)")
# 방법 2: 문장 전체에서 부분 문자열 검색
if any(k in sentence.lower() for k in keywords_set):
print("포함됨 (부분 문자열)")
- 단어 단위 검색: 로그가 공백으로 구분된 경우 최적
- 부분 문자열 검색: 문장 내 임의 위치 검색 가능
2. Aho-Corasick 알고리즘
import ahocorasick
keywords = ["error", "warning", "critical", "timeout", "failed", ...]
keywords = [k.lower() for k in keywords]
A = ahocorasick.Automaton()
for idx, keyword in enumerate(keywords):
A.add_word(keyword, (idx, keyword))
A.make_automaton()
sentence = "System reported CRITICAL failure"
for end_index, (idx, keyword) in A.iter(sentence.lower()):
print("포함:", keyword)
break # 하나라도 찾으면 종료
- 대규모 키워드 검색에 가장 빠른 방법
- 대소문자 무시: 키워드와 문장을 모두 .lower() 처리
3. OR 정규식
import re
keywords = ["error", "warning", "critical", "timeout", "failed", ...]
pattern = re.compile("|".join(map(re.escape, keywords)), re.IGNORECASE)
sentence = "System reported CRITICAL failure"
if pattern.search(sentence):
print("포함됨")
⚠️ 단점: 5100개 키워드라면 정규식이 너무 커져 성능 저하 가능.
📊 방법 비교
| 방법 | 속도 | 메모리 | 적합 상황 |
|---|---|---|---|
| Set 검색 | 빠름 | 중간 | 단순 포함 여부 |
| Aho-Corasick | 매우 빠름 | 중간 | 대규모 키워드 검색 |
| OR 정규식 | 중간 | 높음 | 키워드 수 적을 때 |
🎯 결론
- 5100개 키워드라면 [] 안에 넣는 건 잘못된 방식입니다.
- 실제로는 Set 검색 또는 Aho-Corasick을 쓰는 것이 최적입니다.
- 로그 분석, 보안 탐지, 대규모 텍스트 필터링에 특히 유용합니다.
오늘의 이야기
#스하리1000명프로젝트,
¿Perdido en Corea? Incluso si no hablas coreano, esta aplicación te ayuda a moverte fácilmente.
Simplemente hable su idioma: traduce, busca y muestra resultados en su idioma.
¡Genial para viajeros! Admite más de 10 idiomas, incluidos inglés, japonés, chino, vietnamita y más.
¡Pruébalo ahora!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127
2026/05/19
오늘의 이야기
#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase
🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 “혹시나~” 하면서 봤거든 ㅋㅋ
근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨
그것도 구글 Gemini로다가!
그래서 앱 하나 만들었지
👉 “로또 예상번호 by Gemini” 🎱
AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨
재미로 해도 좋고…
혹시 모르는 거잖아? 😏
https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127
오늘의 이야기
#스하리1000명프로젝트
오늘 내가 만든앱 하나 알려주고 싶어, 이 앱은 알림수집기 라고 이름을 붙였는 데,
내 폰에 표시 되는 알림을 읽어서 내가 지정한 단어가 들어 있고, 지출기록을 남겨야 하는 알림이
있으면 수집하고, 카카오톡으로 친구에게 전달해 주는 기능을 구현해 줄꺼야. 📲
이번 패치에서는 하루 한번 지정한 시간에 나에게 알림(노티) 하도록 기능을 추가 했어. 🙏
한번 써보고 불편한 거 있으면 말해줘.
앱 바로가기
👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao
오늘의 이야기
#스하리1000명프로젝트,
有时候和外劳说话很难,对吧?
我制作了一个简单的应用程序,可以帮助您!你用你的语言写作,其他人用他们的语言看到它。
它根据设置自动翻译。
超级方便,可以轻松聊天。有机会就来看看吧!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416
오늘의 이야기
#billcorea #운동동아리관리앱
🏸 Schneedle,羽毛球俱乐部必备应用!
👉 比洞赛 – 记录分数并寻找对手 🎉
适合任何地方,独自一人、与朋友一起或在俱乐部! 🤝
如果你喜欢羽毛球,一定要尝试一下
前往应用程序👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay
오늘의 이야기
🧾 바코드/QR 영수증 스캐너 앱 개발기 (BarcodeVoucher0407)
일상에서 쉽게 버려지는 영수증들을 스마트하게 관리할 수 있는 바코드/QR 기반 영수증 적립 및 조회 앱을 개발했습니다. 이 앱은 단순히 영수증을 저장하는 것을 넘어, AI 기반 OCR(광학 문자 인식) 로 영수증의 내용을 자동으로 파악하고, 카카오맵과 연동하여 사용처의 위치까지 저장할 수 있는 똑똑한 가계부 역할을 합니다.
🛠 1. 프로젝트 개요 및 기술 스택
🎯 제품 목표
- 바코드/QR 스캔 및 갤러리/카메라 이미지를 통한 영수증 디지털 보관
- Groq AI (Llama 비전 모델) 를 활용한 자동 영수증 파싱 (매장명, 금액, 결제일 등)
- 카카오맵 API를 활용한 매장 위치 시각화 및 저장
- 기간별(월별/일별) 및 카테고리별 지출 통계 대시보드 제공
- Play Core를 활용한 매끄러운 앱 내 업데이트(In-app Update) 제공
💻 기술 스택
- 언어: Kotlin
- UI: Jetpack Compose (단방향 데이터 흐름 및 상태 관리)
- 아키텍처: MVVM + Clean Architecture (Repository, UseCase, 점진적 DTO 분리)
- 데이터베이스: Room (Offline-first 구조)
- 비동기 처리: Coroutines + Flow
- 네트워크: Ktor Client (Groq AI API 연동)
- 의존성 주입: Dagger Hilt
- 기타: Kakao Map API, DataStore(설정 관리), ZXing(바코드 스캔)
🏗 2. 주요 아키텍처와 리팩토링 전략
✅ Clean Architecture 와 UseCase의 도입
앱의 덩치가 커짐에 따라 비즈니스 로직이 ViewModel에 집중되는 현상을 방지하기 위해 ObserveReceiptsUseCase, AnalyzeReceiptImageUseCase 등의 UseCase 계층을 도입했습니다. 이를 통해 로직의 재사용성을 높이고 테스트 용이성을 개선했습니다.
✅ Entity와 DTO의 분리
UI 계층에 Room의 Entity 모델이 직접 노출되는 것을 막기 위해 단계적인 리팩토링을 진행했습니다. 조회용 데이터를 ReceiptSummary, ReceiptDetail과 같은 DTO (Data Transfer Object) 로 매핑하여 도메인 경계를 확실히 구분하였습니다.
🚀 3. 핵심 구현 내용 (주요 코드)
🤖 3.1. Groq AI를 활용한 영수증 OCR과 JSON 파싱 내성 강화
가장 신경 쓴 부분 중 하나는 AI 모델 응답의 불안정성 해결입니다. LLM이 생성한 JSON이 때로는 형식이 깨져서 오거나 불필요한 마크다운 백틱(```)이 붙어오는 문제가 있었습니다. 이를 해결하기 위해 3단계 Fallback 로직을 적용했습니다.
// ReceiptOcrPayloadParser.kt 발췌
private fun parsePayload(rawContent: String): ReceiptOcrPayload {
// 1단계: 마크다운 찌꺼기(BOM, 백틱 등)를 제거하고 JSON 후보 텍스트만 추출
val jsonCandidate = runCatching { rawContent.extractJsonCandidate() }
.getOrElse { rawContent.trim() }
runCatching {
return json.parseToJsonElement(jsonCandidate).jsonObject.toPayload()
}
// 2단계: JSON 파싱 실패 시, 깨진 따옴표나 쉼표를 교정하여 재시도
val repaired = jsonCandidate.repairJsonCandidate()
runCatching {
return json.parseToJsonElement(repaired).jsonObject.toPayload()
}
// 3단계: 모두 실패할 경우 rawText만이라도 보존하여 반환
return ReceiptOcrPayload(
storeName = null, totalAmount = null, currency = null,
purchasedAtIso = null, memo = null,
rawText = rawContent.take(300).trim().ifBlank { null },
)
}
// 텍스트 기반 휴리스틱 분석 (총 금액 추출)
private fun String.extractLikelyTotalAmount(): Long? {
val totalLabelRegex = Regex(
"(\\uCD1D\\s*\\uD569\\uACC4|\\uD569\\uACC4|\\uCD1D\\uC561|\\uACB0\\uC81C\\s*\\uAE08\\uC561)[^0-9]{0,8}([0-9][0-9,]{2,})",
RegexOption.IGNORE_CASE
)
return totalLabelRegex.find(this)?.groupValues?.getOrNull(2)
?.replace(",", "")?.toLongOrNull()
}Tip: AI가 JSON 생성을 완벽히 하지 못하는 경우를 대비해, 응답 평문에서 정규식을 이용해 영수증 금액과 상호명을 2차로 추출하는 안전장치(Fallback)를 두었습니다.
📊 3.2. Compose Canvas로 직접 그린 지출 통계 차트
서드파티 라이브러리에 의존하지 않고, Jetpack Compose의 Canvas와 Animatable을 활용하여 애니메이션이 포함된 바 차트(Bar Chart) 를 직접 구현했습니다.
// StatsScreen.kt 발췌
@Composable
private fun StatBarChart(
labels: List<String>,
values: List<Long>,
primaryColor: Color,
modifier: Modifier = Modifier,
) {
val maxValue = values.max().toFloat().coerceAtLeast(1f)
// 진입 시 아래에서 위로 올라오는 700ms 애니메이션
val animProgress = remember(values) { Animatable(0f) }
LaunchedEffect(values) {
animProgress.snapTo(0f)
animProgress.animateTo(1f, animationSpec = tween(700))
}
Canvas(modifier = modifier.fillMaxWidth().height(190.dp)) {
val chartW = size.width
val barMaxH = size.height * 0.68f
val slotW = chartW / values.size
values.forEachIndexed { idx, value ->
val ratio = (value.toFloat() / maxValue) * animProgress.value
val barH = barMaxH * ratio
// 막대 그리기
drawRoundRect(
color = primaryColor,
topLeft = Offset(slotW * idx + (slotW / 4f), size.height - barH - 20f),
size = Size(slotW / 2f, barH),
cornerRadius = CornerRadius(5.dp.toPx())
)
}
}
}💾 3.3. Room DB를 이용한 강력한 SQLite 집계 통계
앱 내에서 보여지는 월별/일별 지출 통계는 앱 단에서 계산하는 대신, Room 데이터베이스의 SQLite 쿼리를 적극 활용하여 성능을 최적화했습니다.
// ReceiptDao.kt 발췌
@Query("""
SELECT
strftime('%Y-%m', datetime(COALESCE(purchasedAt, createdAt) / 1000, 'unixepoch', 'localtime')) AS month,
SUM(COALESCE(totalAmount, 0)) AS totalAmount,
COUNT(*) AS count
FROM receipts
WHERE (:startMillis IS NULL OR COALESCE(purchasedAt, createdAt) >= :startMillis)
AND (:endMillis IS NULL OR COALESCE(purchasedAt, createdAt) <= :endMillis)
GROUP BY month
ORDER BY month DESC
""")
fun observeMonthlyStats(
startMillis: Long?,
endMillis: Long?
): Flow<List<MonthlyStatRow>>기간 필터까지 DB 단에서 처리하고, 결과를 Flow로 반환받아 UI에 즉시 리액티브하게 반영되도록 구성했습니다.
💡 4. 마무리 및 회고
단일 모듈로 시작하여 MVP를 빠르게 완성한 프로젝트입니다. 개발 과정에서 특히 흥미로웠던 부분은 AI 기반 OCR을 연동하며 발생한 다양한 예외 처리였습니다. AI의 응답은 항상 일관되지 않기 때문에 정규식 Fallback이나 JSON 교정 로직 같은 방어적 프로그래밍이 매우 중요하다는 것을 배웠습니다.
📌 향후 고도화 계획:
- 사용자 맞춤형 커스텀 카테고리 기능 및 도넛(파이) 차트 시각화 추가
- OCR 처리 속도 및 정확도 향상을 위한 AI 모델 A/B 테스트 정교화
- 기능 확장에 대비한 멀티 모듈(
core,feature등) 분리 작업
Jetpack Compose와 Kotlin 최신 스택들을 활용해 클린 아키텍처를 도입해 보는 뜻깊은 경험이었습니다. 🚀
오늘의 이야기
#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase 🎯 야 너 토요일마다 로또 확인하냐? 나도 맨날 “혹시나~” 하면서 봤거든 ㅋㅋ 근데 이제는 그냥 안 해 AI한테 맡겼어 🤖✨ 그것도 구글 Gemini로다가! ...
-
이전 글에서 정리할 것처럼 java에서 kotlin으로 이전을 했습니다. 그러고 나서 보기 시작했는 데, DefaultSharedPrefernces의 사용할 수 없는 환경으로 변경이 된 것을 알게 되었습니다. 이전 prefs = Prefere...