From 7e13a42848b3b08a37f47a6624339c0aac85ec53 Mon Sep 17 00:00:00 2001 From: sb Date: Sun, 5 Jul 2026 14:22:07 +0900 Subject: [PATCH] =?UTF-8?q?feat(account):=20=EC=98=81=EC=88=98=EC=A6=9D=20?= =?UTF-8?q?=EC=82=AC=EC=A7=84=20AI=20=EA=B5=AC=EC=A1=B0=ED=99=94=20?= =?UTF-8?q?=E2=80=94=20=EB=B9=84=EC=A0=84=20=EC=9A=B0=EC=84=A0,=20OCR=20?= =?UTF-8?q?=ED=8F=B4=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 영수증 선택 시 Claude Vision(/receipt-ai)으로 금액·상호·날짜·분류·계좌 자동 채움. AI 실패/미인식 시 기존 Google Vision OCR+규칙파서로 폴백. 결과 배너에 ✨·분류 표시. Co-Authored-By: Claude Opus 4.8 --- src/api/accountApi.js | 15 ++++++++++++-- src/views/account/AccountView.vue | 33 ++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/api/accountApi.js b/src/api/accountApi.js index 579b6bb..f112c90 100644 --- a/src/api/accountApi.js +++ b/src/api/accountApi.js @@ -216,7 +216,7 @@ const realApi = { return http.post('/account/restore', payload) }, - // 영수증 OCR (백엔드가 Google Vision 호출 → 전체 텍스트 반환) + // 영수증 OCR (백엔드가 Google Vision 호출 → 전체 텍스트 반환) — AI 폴백용 ocrReceipt(blob) { const fd = new FormData() fd.append('image', blob, 'receipt.jpg') @@ -225,6 +225,17 @@ const realApi = { timeout: 30000, }) }, + // 영수증 AI 구조화 (Claude Vision) — 금액·상호·날짜·분류·계좌를 한 번에. 사용자 후보 목록을 함께 전달 + parseReceiptAi(blob, { categoryHints = [], walletHints = [] } = {}) { + const fd = new FormData() + fd.append('image', blob, 'receipt.jpg') + categoryHints.forEach((c) => fd.append('categoryHints', c)) + walletHints.forEach((w) => fd.append('walletHints', w)) + return http.post('/account/ocr/receipt-ai', fd, { + headers: { 'Content-Type': 'multipart/form-data' }, + timeout: 30000, + }) + }, } // ===== 둘러보기(데모) 모드 ===== @@ -246,7 +257,7 @@ const DEMO_WRITES = new Set([ 'createRecurring', 'updateRecurring', 'removeRecurring', 'runRecurrings', 'createWallet', 'updateWallet', 'removeWallet', 'reorderWallets', 'createCategory', 'updateCategory', 'removeCategory', 'reorderCategories', 'importCategories', - 'createTag', 'updateTag', 'removeTag', 'reorderTags', 'confirmEntry', 'ocrReceipt', 'restore', + 'createTag', 'updateTag', 'removeTag', 'reorderTags', 'confirmEntry', 'ocrReceipt', 'parseReceiptAi', 'restore', 'createBudget', 'updateBudget', 'removeBudget', 'setExpectedIncome', 'createHolding', 'updateHolding', 'removeHolding', 'addTrade', 'updateTrade', 'removeTrade', 'refreshPrices', 'refreshAllPrices', diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index 3142ed2..e4f0a86 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -250,6 +250,35 @@ async function runOcr(image) { formError.value = null try { const blob = await imageToBlob(image) + // 1) AI 비전 우선 — 금액·상호·날짜·분류·계좌를 한 번에 구조화 + let ai = null + try { + ai = await accountApi.parseReceiptAi(blob, { + categoryHints: allCategoryNames.value, + walletHints: wallets.value.map((w) => w.name), + }) + } catch { + ai = null // AI 실패 → 아래 OCR 폴백 + } + if (ai && ai.recognized) { + form.type = ai.type === 'INCOME' ? 'INCOME' : 'EXPENSE' + if (ai.amount) form.amount = Number(ai.amount) + if (ai.merchant && !form.memo) form.memo = ai.merchant + if (ai.date) form.entryDate = ai.date + if (ai.category) form.category = ai.category + let aiCard = null + if (ai.wallet) { + const w = wallets.value.find((x) => x.name === ai.wallet) + if (w) { + form.walletKind = w.type + form.walletId = w.id + aiCard = w.name + } + } + ocrResult.value = { amount: ai.amount, date: ai.date, store: ai.merchant, card: aiCard, category: ai.category, ai: true } + return + } + // 2) 폴백 — 기존 Google Vision OCR + 규칙 파서 const { text } = await accountApi.ocrReceipt(blob) const r = parseReceiptText(text) // 추출값이 있을 때만 폼에 채움 (메모는 비어있을 때만) @@ -1048,13 +1077,15 @@ onMounted(async () => { 영수증 인식 중… + + 자동 입력됨 - 사진에서 금액·날짜·상호를 자동 입력 + 사진에서 금액·날짜·상호·분류를 자동 입력