Merge branch 'dev'
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { accountApi } from '@/api/accountApi'
|
||||
import IconBtn from '@/components/ui/IconBtn.vue'
|
||||
import { imageToBlob, parseReceiptText } from '@/utils/receiptOcr'
|
||||
@@ -60,29 +60,46 @@ const formError = ref(null)
|
||||
const receiptInput = ref(null)
|
||||
const ocrRunning = ref(false)
|
||||
const ocrResult = ref(null) // { amount, date, store }
|
||||
async function pickReceipt() {
|
||||
const receiptPickerOpen = ref(false) // 카메라/갤러리 선택 레이어
|
||||
const webCapture = ref(false) // 웹: 카메라 버튼이면 capture 활성
|
||||
// 영수증 등록: 커스텀 레이어 팝업(카메라/갤러리 선택)
|
||||
function pickReceipt() {
|
||||
ocrResult.value = null
|
||||
// 네이티브(앱): 카메라/갤러리 선택 프롬프트, 웹: 파일 선택
|
||||
receiptPickerOpen.value = true
|
||||
}
|
||||
async function chooseCamera() {
|
||||
receiptPickerOpen.value = false
|
||||
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 {
|
||||
// 사용자가 취소 → 무시
|
||||
}
|
||||
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 {
|
||||
const photo = await Camera.getPhoto({
|
||||
source,
|
||||
resultType: CameraResultType.DataUrl,
|
||||
quality: 70,
|
||||
correctOrientation: true,
|
||||
})
|
||||
if (photo?.dataUrl) await runOcr(photo.dataUrl)
|
||||
} catch {
|
||||
// 사용자가 취소 → 무시
|
||||
}
|
||||
}
|
||||
async function onReceiptFile(e) {
|
||||
const file = e.target.files?.[0]
|
||||
e.target.value = '' // 같은 파일 재선택 허용
|
||||
@@ -555,6 +572,7 @@ onMounted(async () => {
|
||||
<div v-if="!isRepayment" class="receipt-box">
|
||||
<input
|
||||
ref="receiptInput" type="file" accept="image/*"
|
||||
:capture="webCapture ? 'environment' : undefined"
|
||||
class="receipt-input" @change="onReceiptFile"
|
||||
/>
|
||||
<button
|
||||
@@ -683,6 +701,30 @@ onMounted(async () => {
|
||||
</div>
|
||||
</Transition>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -1000,6 +1042,48 @@ button.primary {
|
||||
0% { margin-left: -40%; }
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user