명함 필드·QR 후보·연락처 내보내기

명함을 연락처에 자동으로 넣는 앱이 아닙니다. OCR·Entity Extraction·연락처 QR 이 후보를 내고 사용자가 필드를 확인·수정하며, 이름이 있는 명함을 저장한 뒤 연락처 앱으로 넘기거나 vCard 로 공유합니다.

소스 기준: 앱 1.0.0 (1), 커밋 ecebf71 · 2026년 9월 23일

범위와 스택

v1 은 한국어·영어 명함의 한 면을 이미지 한 장으로 읽어 앱 안의 명함 목록에 보관하고, 저장한 명함을 연락처 앱이나 vCard 파일로 내보냅니다. 계정·서버·동기화는 없습니다. 도토리 영수증 스캐너와 같은 온디바이스 흐름 위에 만든 두 번째 앱이라, 이 노트는 명함에서 달라지는 점을 중심으로 적습니다.

UIKotlin·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 가 필요 없습니다.

  1. 이미지를 한 번 디코드하고 OCR 과 나란히 QR 읽기를 시작합니다.
  2. OCR 줄은 ML Kit 이 준 그대로 박스와 함께 두고 한 번만 번호를 매깁니다. 모든 후보가 그 ID 를 씁니다.
  3. 한국어·영어 규칙이 필드 후보를 내고, 모델이 이미 기기에 있으면 Entity Extraction 이 전화·이메일·URL·주소 후보를 더합니다.
  4. QR 결과를 기다려 병합한 뒤에 검토 화면을 엽니다. 그래서 늦게 도착한 QR 결과가 사용자가 고치고 있는 필드를 덮을 수 없습니다.
  5. 읽은 값이 하나도 없으면 빈 검토 화면을 열지 않고 인식 실패로 안내합니다.

영수증의 행 묶기를 일부러 쓰지 않습니다. 영수증은 라벨과 금액이 한 행의 양 끝에 있어 같은 높이 조각을 이어야 했습니다. 명함은 T. 02-123-4567 F. 02-123-4568 처럼 한 줄에 여러 필드가 붙어 있어, 이으면 오히려 나누기 어려워집니다. 라벨 기준 분할·단 구분·근접 판정은 이 앱이 맡습니다.

필드 규칙

한국어·영어 사전을 함께 적용하고 별도 언어 판정 단계를 두지 않습니다. 내부적으로 필드 상태는 없음·검토 필요·사용자 확정이며 마지막 상태는 사용자의 수정만 만듭니다. 이 상태는 화면에 표시하지 않습니다. 칸마다 눌러야 하는 확인 표시가 없고 모든 칸은 누르면 바로 고칩니다.

칸마다 값은 하나입니다. 규칙이 어느 칸에도 넣지 못한 조각(로고·문구·못 읽은 주소 조각 등)과 회사·부서·직함·주소의 두 번째 이후 후보는 / 로 이어 메모에 미리 채웁니다. 고르지 않은 대안은 저장하면 사라지기 때문입니다. 뽑힌 이름·다른 표기와, Entity·QR 이 뒤늦게 채운 값은 뺍니다. 대안 칩을 고르면 메모의 그 자리에 밀려난 원래 값을 넣되, 사용자가 메모를 고친 뒤에는 건드리지 않습니다.

초안·확정 같은 기록 상태는 없습니다. 인식 결과는 사용자가 저장하거나 내보낼 때 처음 DB 에 기록되며, 필수 칸인 이름이 채워져 있으면 저장·연락처에 추가·vCard 공유가 열립니다. 이름을 읽지 못한 명함은 이름을 입력해야 저장되고, 회사명을 이름 자리에 넣지 않습니다. 저장하지 않은 결과나 고친 기록을 닫으면 버릴지 묻습니다.

Entity Extraction 과 QR

두 경로 모두 보조입니다. 사용자가 고치지 않은 필드만 채우고, 사용자 수정이나 OCR 주 후보를 자동으로 바꾸지 않습니다.

Gemini Nano(ML Kit GenAI Prompt) 경로는 이름·회사·직함에서 이득이 가장 크지만, v1 에는 debug 전용 가용성 탐침만 있습니다. release 빌드에 Prompt 모듈이 들어가지 않고 생성형 결과가 사용자에게 가지 않습니다.

연락처와 vCard

이름이 채워지면 내보내기 버튼이 열립니다. 누르면 먼저 저장한 뒤 인텐트를 열며, 내보내는 동안에는 저장·닫기를 막습니다.

연락처 앱ContactsContract.Intents.Insert: 이름·회사·직함, 유형을 붙인 첫 전화, 첫 이메일, 주소. 다른 표기 이름·부서·메모·내선과 나머지 전화·이메일과 모든 웹사이트는 메모에 넣습니다.
vCard3.0, UTF-8, CRLF. 전화·이메일·웹사이트를 모두 쓰고, 다른 표기 이름·부서·메모·내선은 NOTE 에 넣습니다.

저장·개인정보·백업

저장·이미지 보관·백업 규칙은 영수증 앱과 같습니다. 다만 명함은 다른 사람의 개인정보라는 점이 더 무겁습니다.

검증과 확인된 제약

추출·전화 규칙·병합·폼 모델·기록 인코딩·연락처 내보내기·내보내기 라벨·vCard 작성은 합성 OCR 텍스트와 가상 인물을 쓰는 JVM 단위 시험으로 확인합니다. 실제 명함은 저장소에 넣지 않습니다. release 빌드는 영수증 앱의 release 전용 크래시를 고친 ML Kit 등록자 보존 규칙을 그대로 물려받습니다.

실기에서 나온 세 가지 수정. Android 11 시험 폰에서 문제 세 개가 나왔습니다. OCR 이 @ 양옆에 공백을 넣어, 이제 띄어진 이메일을 합치되 같은 모양의 SNS 핸들과 구별하려고 도메인이 흔한 최상위 도메인으로 끝날 때만 합칩니다. htps:// 같은 오인식 스킴 조각이 이름 대안으로 새어 들어와, 교정하지 않고 웹 범위로 가립니다. 그리고 내보내는 전화는 국가번호가 붙은 정규화 값일 때만 그 값을 쓰고, 그 밖에는 인쇄 표기를 유지하며 내선은 메모로 옮깁니다.