fix: 앱 잠금이 카메라/갤러리/공유 복귀 시 오작동하던 문제 수정
CI / build (push) Failing after 15m40s

- 원인: visibilitychange(숨김)로 백그라운드를 감지하는데 카메라/파일선택/공유
  호출 시에도 웹뷰가 숨겨져 잠금이 걸렸음(복귀 시 PIN 화면)
- appLock.suppressLock(): 네이티브 동작 직전 일시중지 창을 열어 그 복귀는 잠그지 않음
- 파일선택(input[type=file])은 App.vue 전역 클릭 리스너로 일괄 처리
- 카메라(OCR)·공유(백업)는 호출부에서 직접 suppressLock 호출
- 복귀(visible) 시 일시중지 창 해제

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ByungCheol
2026-06-29 20:25:50 +09:00
parent ce7488076a
commit a0aba1d578
4 changed files with 27 additions and 1 deletions
+9
View File
@@ -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)
})
+14 -1
View File
@@ -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
+2
View File
@@ -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 {
/* 공유 취소 — 파일은 캐시에 저장됨 */
+2
View File
@@ -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,