From e1d2ec5bd73a5f5fa41fcb715c3133eefcd3cbd1 Mon Sep 17 00:00:00 2001 From: ByungCheol Date: Wed, 3 Jun 2026 16:50:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=98=81=EC=88=98=EC=A6=9D=20=EC=8A=A4?= =?UTF-8?q?=EC=BA=94=20=EB=84=A4=EC=9D=B4=ED=8B=B0=EB=B8=8C=20=EC=B9=B4?= =?UTF-8?q?=EB=A9=94=EB=9D=BC(@capacitor/camera)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 앱에서 촬영/갤러리 선택 프롬프트로 영수증 입력 (웹은 파일선택 폴백) - AndroidManifest CAMERA 권한 추가 Co-Authored-By: Claude Opus 4.8 --- android/app/capacitor.build.gradle | 1 + android/app/src/main/AndroidManifest.xml | 4 +++ android/capacitor.settings.gradle | 3 +++ package-lock.json | 10 +++++++ package.json | 1 + src/views/account/AccountView.vue | 33 ++++++++++++++++++++---- 6 files changed, 47 insertions(+), 5 deletions(-) diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index bf02646..f451109 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -10,6 +10,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-app') + implementation project(':capacitor-camera') implementation project(':capacitor-preferences') } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b06ddbf..bcc4816 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -38,4 +38,8 @@ + + + + diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 543170e..c6772c6 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -5,5 +5,8 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/ include ':capacitor-app' project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') +include ':capacitor-camera' +project(':capacitor-camera').projectDir = new File('../node_modules/@capacitor/camera/android') + include ':capacitor-preferences' project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') diff --git a/package-lock.json b/package-lock.json index f7983b3..8bf7e5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@capacitor/android": "^8.3.4", "@capacitor/app": "^8.1.0", + "@capacitor/camera": "^8.2.0", "@capacitor/cli": "^8.3.4", "@capacitor/core": "^8.3.4", "@capacitor/preferences": "^8.0.1", @@ -542,6 +543,15 @@ "@capacitor/core": ">=8.0.0" } }, + "node_modules/@capacitor/camera": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@capacitor/camera/-/camera-8.2.0.tgz", + "integrity": "sha512-hYfrT6xpL936qoEkIpJzSnb0fQCaTkOux1cXzGBfH8QLOGqr6gSLiWZlZz/fqMPmMKJMNRBqlTQkj5fuMhVZog==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + } + }, "node_modules/@capacitor/cli": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.4.tgz", diff --git a/package.json b/package.json index ba673b0..c0c6a6e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "@capacitor/android": "^8.3.4", "@capacitor/app": "^8.1.0", + "@capacitor/camera": "^8.2.0", "@capacitor/cli": "^8.3.4", "@capacitor/core": "^8.3.4", "@capacitor/preferences": "^8.0.1", diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index 5c202e1..5773663 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -3,6 +3,8 @@ import { computed, onMounted, reactive, ref } from 'vue' import { accountApi } from '@/api/accountApi' import IconBtn from '@/components/ui/IconBtn.vue' import { scanReceipt } from '@/utils/receiptOcr' +import { Capacitor } from '@capacitor/core' +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera' const now = new Date() const year = ref(now.getFullYear()) @@ -59,21 +61,42 @@ const receiptInput = ref(null) const ocrRunning = ref(false) const ocrProgress = ref(0) const ocrResult = ref(null) // { amount, date, store } -function pickReceipt() { +async function pickReceipt() { ocrResult.value = null - receiptInput.value?.click() + // 네이티브(앱): 카메라/갤러리 선택 프롬프트, 웹: 파일 선택 + if (Capacitor.isNativePlatform()) { + try { + const photo = await Camera.getPhoto({ + source: CameraSource.Prompt, + resultType: CameraResultType.DataUrl, + quality: 70, + correctOrientation: true, + promptLabelHeader: '영수증', + promptLabelPhoto: '갤러리에서 선택', + promptLabelPicture: '카메라로 촬영', + promptLabelCancel: '취소', + }) + if (photo?.dataUrl) await runOcr(photo.dataUrl) + } catch { + // 사용자가 취소 → 무시 + } + } else { + receiptInput.value?.click() + } } async function onReceiptFile(e) { const file = e.target.files?.[0] e.target.value = '' // 같은 파일 재선택 허용 - if (!file) return + if (file) await runOcr(file) +} +async function runOcr(image) { ocrRunning.value = true ocrProgress.value = 0 ocrResult.value = null formError.value = null try { - const r = await scanReceipt(file, (p) => (ocrProgress.value = p)) - // 추출값이 있을 때만 폼에 채움 (기존 입력 보존하되, 비어있으면 채움) + const r = await scanReceipt(image, (p) => (ocrProgress.value = p)) + // 추출값이 있을 때만 폼에 채움 (메모는 비어있을 때만) if (r.amount) form.amount = r.amount if (r.date) form.entryDate = r.date if (r.store && !form.memo) form.memo = r.store