범위와 스택
v1 은 한국어·영어 명함의 한 면을 이미지 한 장으로 읽어 앱 안의 명함 목록에 보관하고, 저장한 명함을 연락처 앱이나 vCard 파일로 내보냅니다. 계정·서버·동기화는 없습니다. 도토리 영수증 스캐너와 같은 온디바이스 흐름 위에 만든 두 번째 앱이라, 이 노트는 명함에서 달라지는 점을 중심으로 적습니다.
| UI | Kotlin·Jetpack Compose, minSdk 26, targetSdk 36 |
|---|---|
| 입력 | ML Kit 문서 스캐너(한 페이지) 또는 Android Photo Picker(이미지 한 장) |
| OCR | 앱에 포함된 ML Kit Text Recognition v2 한국어 모델 — 라틴 문자도 읽음 |
| 보조 경로 | ML Kit Entity Extraction(한국어·영어 모델)과 연락처 QR 용 Barcode Scanning |
| 저장 | Room 데이터베이스와 앱 전용 이미지 파일 |
| 내보내기 | 연락처 추가 인텐트, 공유 시트로 보내는 vCard 3.0 |
두 번째 소비 앱이 생기면서 영수증 앱에서 순수 Kotlin 코어 모듈 libs/ai/core 를 추출했습니다. 여기에는 OCR 줄·문서 타입, 필드 상태·후보 타입, 인용한 줄 ID 가 실제로 있는지 보는 검사만 있습니다. 명함 규칙·필드 모델·병합 정책은 이 앱에 둡니다.
인식 흐름
입력·staging·권한은 영수증 앱을 따릅니다. 두 버튼을 따로 두고, 스캐너·선택기 URI 를 저장하지 않으며, EXIF 방향대로 바로 세워 디코드하고, 앱 manifest 에 위험 권한이 없고 최종 판정은 release merged manifest 로 합니다. 연락처 추가도 연락처 앱의 편집 화면을 여는 것뿐이라 WRITE_CONTACTS 가 필요 없습니다.
- 이미지를 한 번 디코드하고 OCR 과 나란히 QR 읽기를 시작합니다.
- OCR 줄은 ML Kit 이 준 그대로 박스와 함께 두고 한 번만 번호를 매깁니다. 모든 후보가 그 ID 를 씁니다.
- 한국어·영어 규칙이 필드 후보를 내고, 모델이 이미 기기에 있으면 Entity Extraction 이 전화·이메일·URL·주소 후보를 더합니다.
- QR 결과를 기다려 병합한 뒤에 검토 화면을 엽니다. 그래서 늦게 도착한 QR 결과가 사용자가 고치고 있는 필드를 덮을 수 없습니다.
- 읽은 값이 하나도 없으면 빈 검토 화면을 열지 않고 인식 실패로 안내합니다.
영수증의 행 묶기를 일부러 쓰지 않습니다. 영수증은 라벨과 금액이 한 행의 양 끝에 있어 같은 높이 조각을 이어야 했습니다. 명함은 T. 02-123-4567 F. 02-123-4568 처럼 한 줄에 여러 필드가 붙어 있어, 이으면 오히려 나누기 어려워집니다. 라벨 기준 분할·단 구분·근접 판정은 이 앱이 맡습니다.
필드 규칙
한국어·영어 사전을 함께 적용하고 별도 언어 판정 단계를 두지 않습니다. 내부적으로 필드 상태는 없음·검토 필요·사용자 확정이며 마지막 상태는 사용자의 수정만 만듭니다. 이 상태는 화면에 표시하지 않습니다. 칸마다 눌러야 하는 확인 표시가 없고 모든 칸은 누르면 바로 고칩니다.
- 이름. 명시적 이름 라벨, 같은 줄이나 이웃 줄의 직함, 2~4음절 한글 모양, 띄어 쓴 로마자 이름, 회사와의 근접, 큰 글자 박스를 점수 힌트로 씁니다. 로고나 회사명이 가장 클 수 있어 이것들은 제외 조건이 아니라 점수입니다.
홍 길 동처럼 자간이 벌어진 이름은 원문을 보존하고 후보 값만 정규화하며,대표이사 홍길동은 직함과 이름으로 나눕니다. 점수로 가를 수 없으면 공백을 뺀 세 글자 한글(성 + 이름)을 앞세우며,가 상민처럼 섞어 띄운 표기도 붙입니다. - 학위.
공학박사·공학 박사·박사과정·석사수료·Ph.D.·MBA같은 학위는 이름 후보에서 빼고 직함에직위 / 학위로 붙입니다. 직함이 없으면 학위만 둡니다. 이 규칙 전에는이사 / 공학박사의 학위가 실기기에서 실제 이름을 이겼습니다. - 회사·직함.
(주)·주식회사·Co., Ltd.·Inc.·LLC같은 회사 표지와 두 언어의 직함 사전을 씁니다. 이메일 도메인에서 회사명을 지어내지 않고, 회사명을 이름 자리에 옮기지 않습니다. - 전화. 두 언어의 휴대·사무실·직통·팩스·대표·내선 라벨을 값마다 따로 보존하며, 한국 명함의
C.P·CP는 휴대로 읽습니다. 인쇄 원문과 정규화 값을 따로 저장합니다. 국가번호는 인쇄돼 있거나 사용자가 설정에서 기본 국가를 골랐을 때만 붙입니다. 해외 지사 번호일 수 있으므로 한국어 명함이라는 이유로+82를 붙이지 않습니다. 국제번호 뒤의(0)은 버리지 않고 검토 대상으로 둡니다. - 이메일·웹. 이메일 도메인과 웹 주소가 달라도 오류로 보지 않습니다. 그룹사·외주·메일 서비스에서 흔한 일이라 둘 다 후보로 둡니다.
- 다른 표기 이름.
Hong Gil-dong / 홍길동처럼 한 면에 두 표기가 있으면 주 이름과 다른 표기 이름으로 함께 둘 수 있습니다. 다른 표기 이름을 발음 표기로 보지 않고, 앱이 로마자 표기나 번역을 만들지 않습니다.
칸마다 값은 하나입니다. 규칙이 어느 칸에도 넣지 못한 조각(로고·문구·못 읽은 주소 조각 등)과 회사·부서·직함·주소의 두 번째 이후 후보는 / 로 이어 메모에 미리 채웁니다. 고르지 않은 대안은 저장하면 사라지기 때문입니다. 뽑힌 이름·다른 표기와, Entity·QR 이 뒤늦게 채운 값은 뺍니다. 대안 칩을 고르면 메모의 그 자리에 밀려난 원래 값을 넣되, 사용자가 메모를 고친 뒤에는 건드리지 않습니다.
초안·확정 같은 기록 상태는 없습니다. 인식 결과는 사용자가 저장하거나 내보낼 때 처음 DB 에 기록되며, 필수 칸인 이름이 채워져 있으면 저장·연락처에 추가·vCard 공유가 열립니다. 이름을 읽지 못한 명함은 이름을 입력해야 저장되고, 회사명을 이름 자리에 넣지 않습니다. 저장하지 않은 결과나 고친 기록을 닫으면 버릴지 묻습니다.
Entity Extraction 과 QR
두 경로 모두 보조입니다. 사용자가 고치지 않은 필드만 채우고, 사용자 수정이나 OCR 주 후보를 자동으로 바꾸지 않습니다.
- Entity Extraction 은 모델이 이미 기기에 있을 때만 부르고, 이를 요청마다 다시 확인합니다. 모델이 없으면 그 요청은 Entity 없이 진행하고 다운로드는 Wi-Fi 에서만 요청해, 사용자가 누르지 않은 다운로드가 모바일 데이터를 쓰지 않게 합니다. 모델을 준비하는 동안 홈 화면에 한 줄로 알립니다. 다운로드는 ML Kit 모델 서버 접속일 뿐 명함 내용을 보내지 않습니다.
- OCR 텍스트 밖을 가리키는 Entity 범위는 그 경로에서만 무시하고 OCR 결과는 그대로 씁니다. Entity 전화번호는 박스가 인접하면 윗줄의 라벨을 물려받을 수 있습니다.
- QR 읽기는 OCR 과 독립입니다. OCR 이 실패해도 연락처 QR 이 정확히 하나 있으면 그것으로 검토 화면을 채울 수 있습니다.
- QR 값이 인쇄 값과 같으면 일치를 기록합니다. 다르면 인쇄 값을 주 후보로 두고 QR 값을 대안으로 넣어 인쇄·QR 충돌로 표시합니다. QR 결과는 사용자 수정으로 치지 않습니다.
- 연락처 QR 이 여럿이면 하나로 합치지 않습니다. URL QR 은 별도 링크 후보로 두며, 자동으로 열거나 따라가지 않고 연락처의 웹사이트로 내보내지도 않습니다. 링크 행에는 브라우저 아이콘 버튼(접근성 이름 "링크 열기")이 있어, 사용자가 누를 때만, http·https 주소일 때만 Android 에 넘겨 엽니다.
intent:·tel:·javascript:같은 다른 scheme 에는 버튼이 없고, scheme 이 없는 값에는https://를 붙이며, QR 의 대문자 scheme 은 소문자로 바꿉니다. - QR 후보는 자기 출처와 순번을 갖고 OCR 줄 ID 로 위장하지 않습니다. 그래서 공용 "인용한 줄 존재" 검사는 OCR 후보에만 적용됩니다.
Gemini Nano(ML Kit GenAI Prompt) 경로는 이름·회사·직함에서 이득이 가장 크지만, v1 에는 debug 전용 가용성 탐침만 있습니다. release 빌드에 Prompt 모듈이 들어가지 않고 생성형 결과가 사용자에게 가지 않습니다.
연락처와 vCard
이름이 채워지면 내보내기 버튼이 열립니다. 누르면 먼저 저장한 뒤 인텐트를 열며, 내보내는 동안에는 저장·닫기를 막습니다.
| 연락처 앱 | ContactsContract.Intents.Insert: 이름·회사·직함, 유형을 붙인 첫 전화, 첫 이메일, 주소. 다른 표기 이름·부서·메모·내선과 나머지 전화·이메일과 모든 웹사이트는 메모에 넣습니다. |
|---|---|
| vCard | 3.0, UTF-8, CRLF. 전화·이메일·웹사이트를 모두 쓰고, 다른 표기 이름·부서·메모·내선은 NOTE 에 넣습니다. |
- 연락처 편집 화면을 연 것을 저장 완료로 보지 않습니다. 사용자는 그 화면에서 취소할 수 있습니다.
- 전용 키를 쓰는 것은 첫 전화·이메일뿐입니다. 나머지 값은
Insert.DATA로 넘길 수 있지만 편집기가 표시하지 않는 데이터는 버려질 수 있다고 플랫폼 문서가 밝히므로, Samsung·Google 연락처 앱에서 실측하기 전까지는 메모를 보존 경로로 씁니다. - 다른 표기 이름을 발음 표기 칸에 넣지 않습니다.
- vCard 는 성과 이름을 추측해 나누지 않습니다. 전체 이름을
FN과N의 이름 칸에 둡니다. - 텍스트 값은 역슬래시·쉼표·세미콜론·개행을 이스케이프하고, URL 은 텍스트 이스케이프가 아니라 URI 로 씁니다. 줄은 앞 공백까지 세어 75옥텟에서 접고 서로게이트 쌍을 나누지 않습니다.
- 메모 라벨은 앱에서 고른 언어로 씁니다. 영어 화면 사용자의 연락처에 한국어 라벨이 들어가지 않습니다.
- vCard 파일 이름에는 사람 이름 대신 시각을 넣어, 받는 앱의 기록에 이름이 남지 않게 합니다. 파일은 앱 캐시에서 FileProvider URI 와 임시 읽기 권한으로 공유하며, 24시간이 지난 파일은 내보낼 때와 앱을 시작할 때 지웁니다.
저장·개인정보·백업
저장·이미지 보관·백업 규칙은 영수증 앱과 같습니다. 다만 명함은 다른 사람의 개인정보라는 점이 더 무겁습니다.
- 명함은 Room 테이블 하나에, 이미지는 앱 전용 저장소에 둡니다. 갤러리에 쓰지 않습니다. 명함 삭제는 이미지까지 지우는 영구 삭제입니다. 목록 행마다 체크박스가 있고, 명함을 고르면
삭제 (N)버튼이 나와 확인 뒤 한 문장으로 한꺼번에 지웁니다. - DB 버전 2 는 초안·확정 구분을 담던
status칸을 지웠습니다. Room 자동 migration 이 칸을 지우고 기존 기록은 그대로 둡니다. - 여러 값 필드는 값·라벨·내선·정규화 값·사용자가 고쳤는지만 남기는 길이 접두 인코딩으로 저장합니다. OCR 텍스트·후보 근거·QR 식별자는 저장하지 않으므로 근거 하이라이트는 인식 직후 검토에서만 있습니다. 대안은 저장한 메모에 글자로 남은 만큼만 남습니다.
- 이미지는
noBackupFilesDir아래에 두고, DB 는fullBackupContent·dataExtractionRules로 클라우드 백업과 기기 간 이전에서 뺍니다. - 로그·예외 메시지·광고 요청에 이름·전화·이메일·회사를 넣지 않습니다.
- 보관한 이미지에는 명함에 인쇄된 내용이 모두 남습니다. 이미지 자동 비식별화는 약속하지 않습니다.
검증과 확인된 제약
추출·전화 규칙·병합·폼 모델·기록 인코딩·연락처 내보내기·내보내기 라벨·vCard 작성은 합성 OCR 텍스트와 가상 인물을 쓰는 JVM 단위 시험으로 확인합니다. 실제 명함은 저장소에 넣지 않습니다. release 빌드는 영수증 앱의 release 전용 크래시를 고친 ML Kit 등록자 보존 규칙을 그대로 물려받습니다.
실기에서 나온 세 가지 수정. Android 11 시험 폰에서 문제 세 개가 나왔습니다. OCR 이 @ 양옆에 공백을 넣어, 이제 띄어진 이메일을 합치되 같은 모양의 SNS 핸들과 구별하려고 도메인이 흔한 최상위 도메인으로 끝날 때만 합칩니다. htps:// 같은 오인식 스킴 조각이 이름 대안으로 새어 들어와, 교정하지 않고 웹 범위로 가립니다. 그리고 내보내는 전화는 국가번호가 붙은 정규화 값일 때만 그 값을 쓰고, 그 밖에는 인쇄 표기를 유지하며 내선은 메모로 옮깁니다.
- 이미지 한 장당 명함 한 면만 받습니다. 앞·뒷면 병합, 한 이미지의 여러 명함, 검색·정렬·그룹·썸네일은 v1 범위 밖입니다.
- 기존 연락처와의 중복 확인은
READ_CONTACTS가 필요해 하지 않습니다. 개발팀장처럼 붙여 쓴 복합 직함은 아직 직함으로 잡히지 않고, 다른 표기 이름 규칙은 대문자 로마자 이름을 놓칠 수 있습니다. 검토 화면에서 사용자가 확인합니다.- 내선은 표시만 되고 따로 편집할 수 없으며, 연락처 앱이 없는 기기에서 안내 문구가 나오지 않습니다.
- Entity Extraction 의 한국어 주소 품질은 아직 실측할 가설입니다.