diff --git a/package-lock.json b/package-lock.json
index 8ab5f16..f7983b3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"pinia": "^3.0.4",
"prismjs": "^1.30.0",
"sortablejs": "^1.15.7",
+ "tesseract.js": "^7.0.0",
"vue": "^3.5.32",
"vue-router": "^5.0.4"
},
@@ -2250,6 +2251,12 @@
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/bmp-js": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
+ "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==",
+ "license": "MIT"
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -3274,6 +3281,12 @@
"node": ">= 6"
}
},
+ "node_modules/idb-keyval": {
+ "version": "6.2.5",
+ "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.5.tgz",
+ "integrity": "sha512-eKQkTnS0relYsSOYomx8ozIbmdsQCKUdhyuIaQ2DZgKuaxtyQQMkyD/wlnQN32pO3yutN1b1L8uqwcDKaJd7/Q==",
+ "license": "Apache-2.0"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3389,6 +3402,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-url": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
+ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
+ "license": "MIT"
+ },
"node_modules/is-what": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
@@ -4043,6 +4062,26 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.46",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
@@ -4168,6 +4207,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/opencollective-postinstall": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
+ "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
+ "license": "MIT",
+ "bin": {
+ "opencollective-postinstall": "index.js"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -4669,6 +4717,12 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT"
+ },
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
@@ -4989,6 +5043,30 @@
"node": ">=18"
}
},
+ "node_modules/tesseract.js": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-7.0.0.tgz",
+ "integrity": "sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bmp-js": "^0.1.0",
+ "idb-keyval": "^6.2.0",
+ "is-url": "^1.2.4",
+ "node-fetch": "^2.6.9",
+ "opencollective-postinstall": "^2.0.3",
+ "regenerator-runtime": "^0.13.3",
+ "tesseract.js-core": "^7.0.0",
+ "wasm-feature-detect": "^1.8.0",
+ "zlibjs": "^0.3.1"
+ }
+ },
+ "node_modules/tesseract.js-core": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-7.0.0.tgz",
+ "integrity": "sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw==",
+ "license": "Apache-2.0"
+ },
"node_modules/through2": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
@@ -5024,6 +5102,12 @@
"node": ">=6"
}
},
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -5582,12 +5666,34 @@
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
},
+ "node_modules/wasm-feature-detect": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
+ "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"license": "MIT"
},
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5747,6 +5853,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zlibjs": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
+ "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
}
}
}
diff --git a/package.json b/package.json
index 51f7f0c..ba673b0 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"pinia": "^3.0.4",
"prismjs": "^1.30.0",
"sortablejs": "^1.15.7",
+ "tesseract.js": "^7.0.0",
"vue": "^3.5.32",
"vue-router": "^5.0.4"
},
diff --git a/src/utils/receiptOcr.js b/src/utils/receiptOcr.js
new file mode 100644
index 0000000..cb28977
--- /dev/null
+++ b/src/utils/receiptOcr.js
@@ -0,0 +1,95 @@
+// 영수증 OCR (온디바이스, Tesseract.js). 서버·API키 불필요.
+// 이미지 → 텍스트 인식 → 금액·날짜·상호 추출.
+import Tesseract from 'tesseract.js'
+
+// 합계 금액을 가리키는 키워드 (우선순위 높은 순)
+const TOTAL_KEYWORDS = [
+ '받을금액', '받을 금액', '결제금액', '결제 금액', '합계금액', '합 계', '합계',
+ '총액', '총 액', '총구매액', '판매금액', '결제대상금액', '청구금액', '승인금액',
+ 'total', 'TOTAL',
+]
+// 합계로 오인하기 쉬운(제외할) 키워드
+const EXCLUDE_KEYWORDS = ['부가세', '면세', '과세', '봉사료', '거스름', '받은금액', '현금', '잔액', '포인트']
+
+// 문자열에서 숫자(천단위 콤마 포함)들을 추출 → 정수 배열
+function numbersIn(line) {
+ const out = []
+ const re = /\d{1,3}(?:,\d{3})+|\d{3,}/g
+ let m
+ while ((m = re.exec(line)) !== null) {
+ const n = parseInt(m[0].replace(/,/g, ''), 10)
+ if (!Number.isNaN(n)) out.push(n)
+ }
+ return out
+}
+
+// 합계 금액 추정
+function extractAmount(lines) {
+ // 1) 합계 키워드가 있는 줄에서 금액 찾기 (제외 키워드 줄은 건너뜀)
+ for (const kw of TOTAL_KEYWORDS) {
+ for (const line of lines) {
+ if (!line.includes(kw)) continue
+ if (EXCLUDE_KEYWORDS.some((ex) => line.includes(ex))) continue
+ const nums = numbersIn(line)
+ if (nums.length) return Math.max(...nums)
+ }
+ }
+ // 2) 폴백: 전체에서 가장 큰 금액(100원 이상)
+ const all = lines.flatMap(numbersIn).filter((n) => n >= 100 && n < 100_000_000)
+ return all.length ? Math.max(...all) : null
+}
+
+// 거래일 추정 (YYYY-MM-DD 반환)
+function extractDate(text) {
+ // 4자리 연도: 2024-01-05 / 2024.01.05 / 2024년 1월 5일 / 2024/1/5
+ let m = text.match(/(20\d{2})\s*[.\-/년]\s*(\d{1,2})\s*[.\-/월]\s*(\d{1,2})/)
+ if (m) return fmt(m[1], m[2], m[3])
+ // 2자리 연도: 24.01.05
+ m = text.match(/(\d{2})\s*[.\-/]\s*(\d{1,2})\s*[.\-/]\s*(\d{1,2})/)
+ if (m) return fmt('20' + m[1], m[2], m[3])
+ return null
+}
+function fmt(y, mo, d) {
+ const mm = String(Number(mo)).padStart(2, '0')
+ const dd = String(Number(d)).padStart(2, '0')
+ if (Number(mo) < 1 || Number(mo) > 12 || Number(d) < 1 || Number(d) > 31) return null
+ return `${y}-${mm}-${dd}`
+}
+
+// 상호(가게 이름) 추정 — 상단의 한글 포함, 숫자/키워드 없는 첫 줄
+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
+ if (TOTAL_KEYWORDS.some((k) => t.includes(k))) continue
+ if (['영수증', '주소', '사업자', '대표', 'TEL', '전화'].some((k) => t.includes(k))) continue
+ return t
+ }
+ return null
+}
+
+/**
+ * 영수증 이미지에서 금액·날짜·상호 추출.
+ * @param {File|Blob|string} image 파일 또는 dataURL
+ * @param {(p:number)=>void} onProgress 0~100 진행률 콜백
+ * @returns {Promise<{amount:number|null, date:string|null, store:string|null, text:string}>}
+ */
+export async function scanReceipt(image, onProgress) {
+ const { data } = await Tesseract.recognize(image, 'kor+eng', {
+ logger: (msg) => {
+ if (msg.status === 'recognizing text' && typeof msg.progress === 'number') {
+ onProgress?.(Math.round(msg.progress * 100))
+ }
+ },
+ })
+ const text = data.text || ''
+ const lines = text.split('\n').map((l) => l.trim()).filter(Boolean)
+ return {
+ amount: extractAmount(lines),
+ date: extractDate(text),
+ store: extractStore(lines),
+ text,
+ }
+}
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index 7c39857..4756bd6 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -1,33 +1,454 @@
{{ monthLabel }} 가계부 요약 {{ error }} 슬림하게 관리하는 가계부 · 자산 · 예산 로그인하면 나의 가계부 요약을 볼 수 있어요.안녕하세요, {{ displayName }}님 👋
+ Slim Budget
+
+
+