feat: 영수증 OCR 상호명→메모, 카드사 감지→카드 자동선택
CI / build (push) Failing after 15m1s

- 상호 추출 개선(GS25 등 숫자 포함 상호 허용, 날짜·전화·사업자·주소 제외)
- 카드 결제 영수증의 카드사 감지 → 등록된 카드(issuer/name 매칭) 자동 선택
- 매칭 실패해도 카드결제면 계좌종류를 카드로 좁혀줌
- 결과 표시에 💳카드명 추가

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-03 17:56:18 +09:00
parent 41d6f11531
commit 537cea31b0
2 changed files with 58 additions and 10 deletions
+36 -9
View File
@@ -98,25 +98,52 @@ function fmt(y, mo, d) {
return `${y}-${mm}-${dd}`
}
// 상호(가게 이름) 추정 — 상단의 글자 줄 (날짜·전화·사업자·주소·키워드 제외, GS25 등 숫자 포함 상호는 허용)
const ADDR_HINTS = ['읍', '면', '번지', '아파트']
const STORE_STOP = ['영수증', '주소', '사업자', '대표', 'TEL', '전화', '구매', '승인', '매출', '신용', '카드', '거래']
function extractStore(lines) {
for (const line of lines.slice(0, 6)) {
const t = line.trim()
if (t.length < 2 || t.length > 30) continue
if (/\d/.test(t)) continue
if (!/[가-힣]/.test(t)) continue
for (const raw of lines.slice(0, 6)) {
const t = raw.trim()
if (t.length < 2 || t.length > 25) continue
if (!/[가-힣A-Za-z]/.test(t)) continue // 글자가 있어야
if (/^[\d,\s.\-:]+$/.test(t)) continue // 숫자/기호만
if (/\d{2,4}[.\-/]\d{1,2}[.\-/]\d{1,2}/.test(t)) continue // 날짜
if (/\d{2,4}-\d{3,4}-\d{4}/.test(t)) continue // 전화
if (/\d{3}-\d{2}-\d{5}/.test(t)) continue // 사업자번호
if (TOTAL_KEYWORDS.some((k) => t.includes(k))) continue
if (['영수증', '주소', '사업자', '대표', 'TEL', '전화'].some((k) => t.includes(k))) continue
if (STORE_STOP.some((k) => t.includes(k))) continue
if (ADDR_HINTS.some((k) => t.includes(k)) && /\d/.test(t)) continue // 주소
if (/(시|도|구|군)\s*\S*(로|길|동)\s*\d/.test(t)) continue // 도로명/지번 주소
return t
}
return null
}
/** Vision 이 인식한 전체 텍스트 → { amount, date, store } */
// 카드사 감지 (영수증 텍스트에서). 등록된 카드의 issuer/name 과 매칭하는 데 사용.
const CARD_ISSUERS = [
'카카오뱅크', '삼성', '신한', '현대', '롯데', '하나', '우리', 'KB국민', '국민', 'KB',
'비씨', 'BC', '농협', 'NH', '씨티', '카카오', '토스', '수협', '광주', '전북', '제주',
]
function isCardPayment(text) {
return /카드|신용\s*승인|승인번호|할부|체크\s*승인|일시불/.test(text)
}
function extractCardIssuer(text) {
if (!isCardPayment(text)) return null
for (const c of CARD_ISSUERS) {
if (text.includes(c)) return c
}
return null
}
/** Vision 이 인식한 전체 텍스트 → { amount, date, store, cardIssuer, isCard } */
export function parseReceiptText(text) {
const lines = (text || '').split('\n').map((l) => l.trim()).filter(Boolean)
const t = text || ''
const lines = t.split('\n').map((l) => l.trim()).filter(Boolean)
return {
amount: extractAmount(lines),
date: extractDate(text || ''),
date: extractDate(t),
store: extractStore(lines),
cardIssuer: extractCardIssuer(t),
isCard: isCardPayment(t),
}
}