Merge branch 'dev'
Deploy / deploy (push) Failing after 11m48s
CI / build (push) Failing after 13m1s

This commit is contained in:
ByungCheol
2026-06-03 18:07:04 +09:00
+95 -11
View File
@@ -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,28 +60,45 @@ 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()) {
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: CameraSource.Prompt,
source,
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]
@@ -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;