diff --git a/src/App.vue b/src/App.vue index 2890ba5..d11e213 100644 --- a/src/App.vue +++ b/src/App.vue @@ -76,8 +76,15 @@ function retryReload() { // ===== 앱 잠금(PIN) ===== // 백그라운드로 가면 잠가서, 복귀 시 잠금 화면이 떠 있도록 한다. +// 단, 카메라/갤러리/공유 등 앱 내 네이티브 동작으로 인한 일시 숨김은 제외(suppress). function onVisibility() { if (document.hidden) appLock.lock() + else appLock.clearSuppress() // 복귀 — 일시중지 창 해제 +} +// 파일 선택(input[type=file]) 클릭 시 다음 백그라운드 전환을 잠금에서 제외 +function onFileInputClick(e) { + const t = e.target + if (t && t.tagName === 'INPUT' && t.type === 'file') appLock.suppressLock() } // ===== 토스트 (뒤로가기 종료 안내 등) ===== @@ -100,6 +107,7 @@ onMounted(() => { // 앱 잠금: 저장된 세션으로 재진입하는 경우 시작 시 잠금(새 로그인 직후는 잠그지 않음) if (appLock.state.enabled && appLock.hasSession()) appLock.lock() document.addEventListener('visibilitychange', onVisibility) + document.addEventListener('click', onFileInputClick, true) ui.loadSignupEnabled() // 회원가입 허용 여부 선로딩(랜딩/로그인 가입 버튼에 반영) // 로그인 상태면 전체 프로필(아바타·포인트) 최신화 — 재로그인 없이 헤더 아바타 반영 if (auth.isAuthenticated) { @@ -123,6 +131,7 @@ onUnmounted(() => { window.removeEventListener('net:error', onNetError) window.removeEventListener('app:toast', onToast) document.removeEventListener('visibilitychange', onVisibility) + document.removeEventListener('click', onFileInputClick, true) clearTimeout(netTimer) clearTimeout(toastTimer) }) diff --git a/src/composables/appLock.js b/src/composables/appLock.js index 309780c..f33422d 100644 --- a/src/composables/appLock.js +++ b/src/composables/appLock.js @@ -13,6 +13,10 @@ const state = reactive({ locked: false, }) +// 카메라/갤러리/공유 등 앱 내 네이티브 동작을 호출하면 웹뷰가 잠깐 백그라운드로 +// 인식돼 잠금이 걸리는 오작동을 막기 위한 "잠금 일시중지" 창(timestamp). +let suppressUntil = 0 + async function hashPin(pin) { const data = new TextEncoder().encode('dondwaeji-applock:' + pin) const buf = await crypto.subtle.digest('SHA-256', data) @@ -39,10 +43,19 @@ export const appLock = { state.enabled = false state.locked = false }, - /** 잠금 걸기(활성화된 경우에만) */ + /** 잠금 걸기(활성화 + 일시중지 창 밖일 때만) */ lock() { + if (Date.now() < suppressUntil) return // 네이티브 동작 복귀 — 잠그지 않음 if (state.enabled) state.locked = true }, + /** 다음 백그라운드 전환을 잠그지 않음(카메라/갤러리/공유 호출 직전에 사용) */ + suppressLock(ms = 60000) { + suppressUntil = Date.now() + ms + }, + /** 일시중지 창 해제(앱으로 복귀 시) */ + clearSuppress() { + suppressUntil = 0 + }, /** 잠금 풀기(이번 세션) */ unlock() { state.locked = false diff --git a/src/utils/backup.js b/src/utils/backup.js index dafe109..e70cad1 100644 --- a/src/utils/backup.js +++ b/src/utils/backup.js @@ -2,6 +2,7 @@ // 참조는 이름 기준(계좌명·분류명·태그명)으로 저장해 추후 '가져오기(복구)' 시 재매핑이 단순하도록 한다. import { Capacitor } from '@capacitor/core' import { accountApi } from '@/api/accountApi' +import { appLock } from '@/composables/appLock' async function safe(promise) { try { @@ -25,6 +26,7 @@ async function saveWorkbook(XLSX, wb, name) { const { Share } = await import('@capacitor/share') const res = await Filesystem.writeFile({ path: name, data: base64, directory: Directory.Cache }) try { + appLock.suppressLock() // 공유 시트 복귀 시 앱 잠금 오작동 방지 await Share.share({ title: name, text: '가계부 백업 파일', url: res.uri }) } catch { /* 공유 취소 — 파일은 캐시에 저장됨 */ diff --git a/src/views/account/AccountView.vue b/src/views/account/AccountView.vue index e6eb333..85bd044 100644 --- a/src/views/account/AccountView.vue +++ b/src/views/account/AccountView.vue @@ -4,6 +4,7 @@ import { accountApi } from '@/api/accountApi' import { reminders } from '@/native/reminders' import { useAuthStore } from '@/stores/auth' import { useDialog } from '@/composables/dialog' +import { appLock } from '@/composables/appLock' import IconBtn from '@/components/ui/IconBtn.vue' import { imageToBlob, parseReceiptText } from '@/utils/receiptOcr' import { cardNotif } from '@/native/cardNotif' @@ -136,6 +137,7 @@ async function chooseGallery() { // 네이티브 전용 — 카메라 플러그인을 이 시점에만 동적 로드 async function captureFrom(sourceKey) { try { + appLock.suppressLock() // 카메라 복귀 시 앱 잠금 오작동 방지 const { Camera, CameraResultType, CameraSource } = await import('@capacitor/camera') const photo = await Camera.getPhoto({ source: sourceKey === 'CAMERA' ? CameraSource.Camera : CameraSource.Photos,