- OS 기본 프롬프트 대신 앱 톤의 모달(카메라 촬영 / 갤러리에서 선택 카드) - 네이티브: CameraSource.Camera / Photos 직접 호출, 웹: file input(capture 토글) 폴백 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||||
import { accountApi } from '@/api/accountApi'
|
import { accountApi } from '@/api/accountApi'
|
||||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||||
import { imageToBlob, parseReceiptText } from '@/utils/receiptOcr'
|
import { imageToBlob, parseReceiptText } from '@/utils/receiptOcr'
|
||||||
@@ -60,28 +60,45 @@ const formError = ref(null)
|
|||||||
const receiptInput = ref(null)
|
const receiptInput = ref(null)
|
||||||
const ocrRunning = ref(false)
|
const ocrRunning = ref(false)
|
||||||
const ocrResult = ref(null) // { amount, date, store }
|
const ocrResult = ref(null) // { amount, date, store }
|
||||||
async function pickReceipt() {
|
const receiptPickerOpen = ref(false) // 카메라/갤러리 선택 레이어
|
||||||
|
const webCapture = ref(false) // 웹: 카메라 버튼이면 capture 활성
|
||||||
|
// 영수증 등록: 커스텀 레이어 팝업(카메라/갤러리 선택)
|
||||||
|
function pickReceipt() {
|
||||||
ocrResult.value = null
|
ocrResult.value = null
|
||||||
// 네이티브(앱): 카메라/갤러리 선택 프롬프트, 웹: 파일 선택
|
receiptPickerOpen.value = true
|
||||||
|
}
|
||||||
|
async function chooseCamera() {
|
||||||
|
receiptPickerOpen.value = false
|
||||||
if (Capacitor.isNativePlatform()) {
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
await captureFrom(CameraSource.Camera)
|
||||||
|
} else {
|
||||||
|
webCapture.value = true
|
||||||
|
await nextTick()
|
||||||
|
receiptInput.value?.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function chooseGallery() {
|
||||||
|
receiptPickerOpen.value = false
|
||||||
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
await captureFrom(CameraSource.Photos)
|
||||||
|
} else {
|
||||||
|
webCapture.value = false
|
||||||
|
await nextTick()
|
||||||
|
receiptInput.value?.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function captureFrom(source) {
|
||||||
try {
|
try {
|
||||||
const photo = await Camera.getPhoto({
|
const photo = await Camera.getPhoto({
|
||||||
source: CameraSource.Prompt,
|
source,
|
||||||
resultType: CameraResultType.DataUrl,
|
resultType: CameraResultType.DataUrl,
|
||||||
quality: 70,
|
quality: 70,
|
||||||
correctOrientation: true,
|
correctOrientation: true,
|
||||||
promptLabelHeader: '영수증',
|
|
||||||
promptLabelPhoto: '갤러리에서 선택',
|
|
||||||
promptLabelPicture: '카메라로 촬영',
|
|
||||||
promptLabelCancel: '취소',
|
|
||||||
})
|
})
|
||||||
if (photo?.dataUrl) await runOcr(photo.dataUrl)
|
if (photo?.dataUrl) await runOcr(photo.dataUrl)
|
||||||
} catch {
|
} catch {
|
||||||
// 사용자가 취소 → 무시
|
// 사용자가 취소 → 무시
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
receiptInput.value?.click()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
async function onReceiptFile(e) {
|
async function onReceiptFile(e) {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0]
|
||||||
@@ -555,6 +572,7 @@ onMounted(async () => {
|
|||||||
<div v-if="!isRepayment" class="receipt-box">
|
<div v-if="!isRepayment" class="receipt-box">
|
||||||
<input
|
<input
|
||||||
ref="receiptInput" type="file" accept="image/*"
|
ref="receiptInput" type="file" accept="image/*"
|
||||||
|
:capture="webCapture ? 'environment' : undefined"
|
||||||
class="receipt-input" @change="onReceiptFile"
|
class="receipt-input" @change="onReceiptFile"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -683,6 +701,30 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
|
<!-- 영수증 등록 방식 선택 (카메라/갤러리) -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="receiptPickerOpen" class="modal-backdrop picker-backdrop" @click.self="receiptPickerOpen = false">
|
||||||
|
<div class="modal picker-modal" role="dialog" aria-modal="true">
|
||||||
|
<button class="close" type="button" @click="receiptPickerOpen = false">×</button>
|
||||||
|
<h2>영수증 등록</h2>
|
||||||
|
<div class="picker-options">
|
||||||
|
<button type="button" class="picker-opt" @click="chooseCamera">
|
||||||
|
<span class="po-icon">📷</span>
|
||||||
|
<span class="po-label">카메라 촬영</span>
|
||||||
|
<span class="po-desc">지금 영수증을 찍어요</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="picker-opt" @click="chooseGallery">
|
||||||
|
<span class="po-icon">🖼️</span>
|
||||||
|
<span class="po-label">갤러리에서 선택</span>
|
||||||
|
<span class="po-desc">저장된 사진을 골라요</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1000,6 +1042,48 @@ button.primary {
|
|||||||
0% { margin-left: -40%; }
|
0% { margin-left: -40%; }
|
||||||
100% { margin-left: 100%; }
|
100% { margin-left: 100%; }
|
||||||
}
|
}
|
||||||
|
/* 영수증 등록 방식 선택 팝업 */
|
||||||
|
.picker-backdrop {
|
||||||
|
z-index: 1200; /* 내역 모달(1000) 위에 */
|
||||||
|
}
|
||||||
|
.picker-modal {
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
.picker-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
.picker-opt {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
padding: 1.1rem 0.6rem;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--color-background-soft);
|
||||||
|
color: var(--color-text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, transform 0.1s;
|
||||||
|
}
|
||||||
|
.picker-opt:hover {
|
||||||
|
border-color: hsla(160, 100%, 37%, 0.6);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
.po-icon {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.po-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.po-desc {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
.entry-form label {
|
.entry-form label {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user