범위와 스택
v1 은 한국어·영어 영수증을 이미지 한 장당 한 건으로 읽고, 여러 통화를 환산 없이 다룹니다. 품목이나 카테고리가 아니라 영수증마다 요약 필드를 남깁니다. 계정·서버·클라우드 동기화는 없습니다.
| UI | Kotlin·Jetpack Compose, minSdk 26, targetSdk 36 |
|---|---|
| 입력 | ML Kit 문서 스캐너(한 페이지) 또는 Android Photo Picker(이미지 한 장) |
| OCR | 앱에 포함된 ML Kit Text Recognition v2 한국어 모델 |
| 추출 | 한국어·영어 라벨 사전과 공통 규칙 엔진 — 앱이 소유 |
| 저장 | Room 데이터베이스와 앱 전용 이미지 파일 |
| 공용 코드 | OCR·스캐너용 libs/ai 어댑터와, 명함 앱과 공유하는 순수 Kotlin 코어 계약 |
공용 모듈은 영수증을 모릅니다. 영수증 라벨·금액 타입·병합 정책은 앱 안에 두어, 두 번째 소비 앱이 영수증 전제를 우연히 물려받지 않게 했습니다.
입력과 이미지 수명
시작 화면에 "촬영"과 "사진에서 선택" 두 버튼을 따로 둡니다. 스캐너 취소는 실패가 아니며 사진 선택기를 자동으로 띄우지 않습니다.
- 위험 권한이 없습니다. 문서 스캐너는 Google Play 서비스 안에서 자기 카메라 권한으로 돌고, Photo Picker 는 고른 파일만 넘깁니다. 앱 manifest 에 카메라·저장소 권한을 선언하지 않으며, 최종 판정은 광고 SDK 까지 합친 release merged manifest 로 합니다.
- 스캐너·선택기가 준 URI 를 저장하지 않습니다. 스캐너 파일과 선택기 권한이 모두 임시라서 받는 즉시 앱 전용 staging 영역으로 복사합니다.
- OCR 전에 EXIF 방향대로 이미지를 바로 세워 디코드하므로 인식 박스와 표시 이미지가 같은 좌표를 씁니다.
- 인식 결과는 곧바로 초안으로 저장하고, 초안을 고치면 잠시 뒤 자동 저장합니다. 이미 확정한 기록을 고치는 동안은 자동 저장하지 않아, 고치다 만 값이 확정 기록을 덮거나 초안으로 강등시키지 않습니다.
- 파일 저장과 DB 저장은 한 트랜잭션이 아니므로, 다음 시작 때 남은 staging 파일, 중단된 검토의 임시 이미지, 어떤 기록도 가리키지 않는 이미지 파일을 지웁니다.
첫 실행 오프라인 동작은 앱에 포함된 OCR 에만 해당합니다. 스캐너 모듈은 Play 서비스가 내려받고, 사진 선택기는 클라우드 사진 공급자를 보여줄 수 있습니다. "앱이 영수증을 서버로 보내지 않는다"와 "이미지를 얻을 때도 네트워크를 쓰지 않는다"는 다른 말입니다.
인식과 필드 상태
앱에 포함된 한국어 Text Recognition v2 모델 하나가 한글과 라틴 문자를 함께 읽으므로 영어·혼합 영수증에 모델을 더 넣지 않습니다. 문자를 지원한다는 것과 영수증에서 정확하다는 것은 별개라 영어 영수증은 따로 시험합니다.
OCR 줄을 눈에 보이는 행으로 다시 묶습니다. ML Kit 은 가로 간격이 넓은 글자를 다른 줄로 나눕니다. 영수증은 라벨과 금액이 한 행의 양 끝에 있어서, Android 11 시험 폰에서 합성 한국어 영수증을 읽었을 때 상호·날짜만 나오고 금액·세금이 비었습니다. 지금은 세로 겹침이 작은 쪽 높이의 절반 이상인 조각을 한 행으로 이은 뒤 규칙을 적용합니다.
- 한국어·영어 사전을 모든 영수증에 함께 적용합니다. 언어를 먼저 판정해 한쪽 규칙만 고르지 않습니다.
- 금액 라벨을 역할로 나눕니다.
합계·결제금액·Grand total·Amount paid같은 합계 라벨이 후보이고, 소계·Balance due·Amount due·팁과 제안 팁·서비스료·받은 현금·거스름돈·할인·포인트 같은 구성 항목은 거래금액으로 보지 않습니다. - 세금은 인쇄된 라벨(VAT·Sales tax·GST·
부가세)을 보존합니다. 세금 합계 없이 세금 행이 여럿이면 더하지 않고 검토 대상으로 둡니다. - MM/DD 와 DD/MM 로 다르게 읽히는 날짜는 표시합니다. 연도나 날짜가 없으면 오늘이나 촬영일로 채우지 않습니다.
- 카드번호는 끝 4자리만 필드로 인정합니다.
필드 상태는 없음·검토 필요·사용자 확정 셋이며, 마지막 상태는 사용자만 만듭니다. 사용자가 고친 필드는 이후 인식이 덮지 않습니다. 상호·거래일·금액·통화가 모두 사용자 확정이고 사용자가 확정 저장했을 때만 기록이 확정이 되고, 나머지는 모두 초안입니다.
설계에는 Gemini Nano(ML Kit GenAI Prompt) 경로가 있습니다. 값과 그 근거가 된 OCR 줄 ID 를 받아 앱이 검증한 뒤에만 쓰는 방식입니다. 이 기준 버전에서는 연결하지 않았습니다. release 빌드에 Prompt 모듈이 들어가지 않고, Galaxy S25 에서 debug 전용 가용성 탐침이 UNAVAILABLE 을 돌려줬습니다. 이 버전의 결과는 모두 OCR 과 규칙에서 나옵니다.
금액과 통화
금액은 Money(currencyCode, unscaled: Long, scale: Int) 로 저장합니다. 12.34 USD 는 USD / 1234 / 2 입니다. 자릿수(scale)를 기록마다 저장하고, ICU·CLDR 버전에 따라 바뀔 수 있는 기기의 통화 메타데이터로 다시 해석하지 않습니다.
- 인쇄된 문자열 →
BigDecimal→ 저장 자릿수로, 부동소수점과 반올림 없이 변환합니다.12.345를 조용히12.35로 만들지 않고, 통화의 보통 자릿수와 어긋나면 검토 대상으로 둡니다. - 숫자 파서는 자릿수 구분 위치, 공백과 NBSP, 점·쉼표, 괄호를 명시적으로 검사하고 문자열 전체를 소비했는지 확인합니다. 유효한 해석이 둘이면 둘 다 후보로 남깁니다.
- 통화 근거에는 순위가 있습니다. 금액에 붙은 코드·명확한 표기, 같은 거래 문맥, 모호한 기호, 힌트 순입니다.
$·¥는 모호한 채로 두고, 영어를 USD 로, 한국어를 KRW 로 연결하지 않습니다. - 설정의 주 통화는 제안값일 뿐 영수증에서 확인한 근거로 쓰지 않습니다.
- 금액은 음수가 아닌 크기로 저장하고 거래 종류(구매·환불·미확인)가 합계 부호를 정합니다. 인쇄된 마이너스와 환불 종류가 이중으로 적용되지 않습니다.
실기 시험 뒤 두 규칙을 더했습니다. 영수증의 명시 통화 표기가 모두 같은 한 통화면, 표기 없는 합계 라벨의 문맥 근거로 그 통화를 쓰고 문맥 판정임을 표시해 검토 대상으로 둡니다(합계 줄의 원 은 오인식됐지만 같은 전표의 부가세: 0원 이 남아 있었습니다). 또 통화가 KRW·JPY 처럼 소수 자릿수 0 으로 명시 판정되면 13,500 을 13.5 로 읽는 해석을 뺍니다. 이 예외가 없으면 원화 금액 거의 전부에 가짜 모호성이 붙었습니다. 두 규칙 모두 모호한 기호와 소수 2자리 통화에는 적용하지 않습니다.
합계와 CSV 내보내기
홈 화면은 이번 달을 통화별로 총지출·환불·순액으로 나눠 보여주고 통화 사이를 환산하지 않습니다. 거래 종류가 정해진 확정 기록만 셉니다. 초안과 아직 미확인인 기록은 합계에서 빠집니다.
- 합계는
BigDecimal로 더합니다. 저장된 정수를 그대로 더하면 12.3(scale 1)과 12.30(scale 2)이 10배 차이로 합산됩니다. - CSV 는 확정 영수증 한 건이 한 행이고
schema_version열을 둡니다. 금액은 저장 자릿수에서 만든 정확한 십진 문자열이며 소수점은 점, 자릿수 구분과 기호가 없고 옆 열에 ISO 통화 코드가 있습니다. - 세금은 통화가 거래금액과 같을 때만 쓰고, 다르면 빈 칸입니다.
=·+·-·@로 시작하는 텍스트 칸은 접두를 붙여 스프레드시트 수식 주입을 막고, 앞자리 0 이 있는 숫자 텍스트도 같은 방식으로 보존합니다. 파일은 BOM 이 있는 UTF-8, CRLF 행입니다.- 파일은 앱 캐시에 쓰고 FileProvider URI 와 임시 읽기 권한으로 공유하므로 저장소 권한이 필요 없습니다. 24시간이 지난 내보내기 파일은 내보낼 때와 앱 시작 때 지웁니다.
저장·개인정보·백업
- 기록은 Room 에, 이미지는 앱 전용 저장소에 둡니다. 갤러리에 쓰지 않습니다.
- 이미지는 기본으로 보관합니다. 영수증 삭제는 이미지까지 지우는 영구 삭제입니다. 보관을 끄면 그 뒤에 인식한 영수증은 검토가 끝날 때 지워지는 임시 이미지를 쓰고, 이전에 보관한 이미지는 설정을 꺼도 지우지 않습니다.
- 카드 정보 필드는 끝 4자리뿐이지만 보관한 이미지에는 인쇄된 내용이 그대로 남습니다. 이미지 자동 비식별화는 약속하지 않습니다.
- OCR 전문은 저장하지 않습니다. 근거 하이라이트는 인식 직후 검토에서만 있고, 저장한 기록을 다시 열면 이미지와 저장 필드만 보입니다.
- 이미지는 Auto Backup 이 건너뛰는
noBackupFilesDir아래에 있습니다. DB 는 Android 11 이하fullBackupContent, Android 12 이상dataExtractionRules로 클라우드 백업에서 빼고 기기 간 이전에서도 빼서, 이미지 없는 기록이 옮겨지지 않게 했습니다. - CSV 는 내보내기이지 백업이 아닙니다. v1 에는 복원·동기화가 없습니다.
검증과 확인된 제약
금액·파서·추출·행 묶기·병합·CSV·검토 폼·월 합계 로직은 합성 OCR 텍스트와 가상 영수증을 쓰는 JVM 단위 시험으로 확인합니다. 실제 영수증은 저장소에 넣지 않습니다.
release 에서만 난 크래시. 1.0.0 (1) 빌드는 시작하자마자 죽었고 debug 빌드는 정상이었습니다. ML Kit 이 끌어오는 firebase-components 의 consumer 규칙이 등록자 클래스를 멤버 없이 보존해, R8 full mode 가 인자 없는 생성자를 지웠습니다. 그 결과 구성요소 탐색이 조용히 실패하고 텍스트 인식기가 null 구성요소를 받았습니다. 지금은 libs/ai 의 OCR·스캐너 모듈이 등록자 생성자를 보존하는 규칙을 함께 배포하며, 수정본은 1.0.0 (2) 로 빌드했습니다. 그 버전 코드는 출시 없이 Play Console 에서 소진되어, 출시용 빌드는 1.0.0 (3) 입니다.
- v1 에는 검색, 이번 달 이전의 월 합계, 중복 영수증 알림이 없습니다.
- 품목·카테고리, 여러 장·PDF 입력, 환율 환산, 세무·가계부 형식은 범위 밖입니다.
- 해외 카드 전표에는 가게 통화와 카드 결제 통화가 함께 찍힐 수 있습니다. 앱은 사용자가 고른 금액·통화 한 쌍만 기록하고, 둘을 더하거나 나누지 않습니다.
- GenAI 경로는 지원 기기에서 실측하기 전까지 설계로 남습니다.
- 영어 화면에서 "Month 9" 로 보이던 고정 월 표기를 1.0.0 (3) 에서 로캘의 월 이름으로 바꿨습니다.