Compare commits

..

4 Commits

Author SHA1 Message Date
ByungCheol 8e7955b5d7 Merge branch 'dev'
Deploy / deploy (push) Failing after 14m6s
2026-06-29 20:25:52 +09:00
ByungCheol a0aba1d578 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>
2026-06-29 20:25:50 +09:00
ByungCheol ce7488076a docs(brand): 백로그에 앱잠금-카메라복귀 PIN 오작동 버그 추가 2026-06-29 17:48:19 +09:00
ByungCheol 2eaf8695b0 docs(brand): 출시 후 업데이트 백로그 추가(업데이트안내·신고글관리·포인트정책·분류순서변경보호) 2026-06-29 09:52:42 +09:00
5 changed files with 43 additions and 1 deletions
+16
View File
@@ -98,6 +98,22 @@
--- ---
## 📌 출시 후 업데이트 백로그 (나중에 진행)
> 첫 출시엔 미포함. 정식 출시 후 업데이트로 진행할 항목.
1. **앱 업데이트 안내 메시지** — 새 버전 있을 때 안내 + 구글플레이 바로가기(스토어 페이지로 이동)
2. **관리자 신고글 목록 페이지** — 관리자가 게시판 신고된 글 목록을 확인하는 화면
3. **가계부 작성 포인트** — 가계부 내역 작성 시 건당 10포인트, **일 50포인트 제한**
4. **게시판 포인트 정책 개편**
1) 게시글 작성 30 → **50포인트**, **도배성 게시물은 적립 제외**
2) 게시글 등록 **키워드 필터링**
3) **추천 +5 / 비추 -1** (일일 최대 적립 50포인트)
4) 다른 사용자 글에 **추천 누르면 +1** (일일 10 제한), 추천 **취소 시 -1**
5. **분류 화면 순서변경 보호** — 스크롤하다 실수로 순서 바뀌는 것 방지: 기본은 drag&drop 비활성, **"순서변경" 버튼 클릭 시에만** 드래그 가능
6. **[버그] 앱 잠금이 카메라/갤러리 복귀 시 오작동** — 영수증 OCR(카메라)·이미지 선택 등 앱 내 네이티브 동작 후 돌아오면 PIN 잠금 화면이 뜸. 원인: 앱 잠금이 `visibilitychange`(화면 숨김)으로 백그라운드를 감지하는데, 카메라/갤러리 호출도 숨김으로 인식됨. 수정: 네이티브 동작 직전 "다음 1회 잠금 건너뛰기" 플래그를 세워 복귀 시 잠금 안 걸리게(진짜 앱 이탈만 잠금). (이번 세션 앱잠금 추가로 생긴 회귀)
---
## 다음 세션 재개 지점 ## 다음 세션 재개 지점
**신원확인 승인 여부 확인** → 승인됐으면 "앱 만들기 → 익명 셋업 → 필수설정/등록정보 입력" 진행. **신원확인 승인 여부 확인** → 승인됐으면 "앱 만들기 → 익명 셋업 → 필수설정/등록정보 입력" 진행.
대기 중이면 **테스터 12명 Gmail 확보** + **출시 전 다듬기(빈 상태/오프라인)** 진행 권장. 대기 중이면 **테스터 12명 Gmail 확보** + **출시 전 다듬기(빈 상태/오프라인)** 진행 권장.
+9
View File
@@ -76,8 +76,15 @@ function retryReload() {
// ===== 앱 잠금(PIN) ===== // ===== 앱 잠금(PIN) =====
// 백그라운드로 가면 잠가서, 복귀 시 잠금 화면이 떠 있도록 한다. // 백그라운드로 가면 잠가서, 복귀 시 잠금 화면이 떠 있도록 한다.
// 단, 카메라/갤러리/공유 등 앱 내 네이티브 동작으로 인한 일시 숨김은 제외(suppress).
function onVisibility() { function onVisibility() {
if (document.hidden) appLock.lock() 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() if (appLock.state.enabled && appLock.hasSession()) appLock.lock()
document.addEventListener('visibilitychange', onVisibility) document.addEventListener('visibilitychange', onVisibility)
document.addEventListener('click', onFileInputClick, true)
ui.loadSignupEnabled() // 회원가입 허용 여부 선로딩(랜딩/로그인 가입 버튼에 반영) ui.loadSignupEnabled() // 회원가입 허용 여부 선로딩(랜딩/로그인 가입 버튼에 반영)
// 로그인 상태면 전체 프로필(아바타·포인트) 최신화 — 재로그인 없이 헤더 아바타 반영 // 로그인 상태면 전체 프로필(아바타·포인트) 최신화 — 재로그인 없이 헤더 아바타 반영
if (auth.isAuthenticated) { if (auth.isAuthenticated) {
@@ -123,6 +131,7 @@ onUnmounted(() => {
window.removeEventListener('net:error', onNetError) window.removeEventListener('net:error', onNetError)
window.removeEventListener('app:toast', onToast) window.removeEventListener('app:toast', onToast)
document.removeEventListener('visibilitychange', onVisibility) document.removeEventListener('visibilitychange', onVisibility)
document.removeEventListener('click', onFileInputClick, true)
clearTimeout(netTimer) clearTimeout(netTimer)
clearTimeout(toastTimer) clearTimeout(toastTimer)
}) })
+14 -1
View File
@@ -13,6 +13,10 @@ const state = reactive({
locked: false, locked: false,
}) })
// 카메라/갤러리/공유 등 앱 내 네이티브 동작을 호출하면 웹뷰가 잠깐 백그라운드로
// 인식돼 잠금이 걸리는 오작동을 막기 위한 "잠금 일시중지" 창(timestamp).
let suppressUntil = 0
async function hashPin(pin) { async function hashPin(pin) {
const data = new TextEncoder().encode('dondwaeji-applock:' + pin) const data = new TextEncoder().encode('dondwaeji-applock:' + pin)
const buf = await crypto.subtle.digest('SHA-256', data) const buf = await crypto.subtle.digest('SHA-256', data)
@@ -39,10 +43,19 @@ export const appLock = {
state.enabled = false state.enabled = false
state.locked = false state.locked = false
}, },
/** 잠금 걸기(활성화된 경우에만) */ /** 잠금 걸기(활성화 + 일시중지 창 밖일 때만) */
lock() { lock() {
if (Date.now() < suppressUntil) return // 네이티브 동작 복귀 — 잠그지 않음
if (state.enabled) state.locked = true if (state.enabled) state.locked = true
}, },
/** 다음 백그라운드 전환을 잠그지 않음(카메라/갤러리/공유 호출 직전에 사용) */
suppressLock(ms = 60000) {
suppressUntil = Date.now() + ms
},
/** 일시중지 창 해제(앱으로 복귀 시) */
clearSuppress() {
suppressUntil = 0
},
/** 잠금 풀기(이번 세션) */ /** 잠금 풀기(이번 세션) */
unlock() { unlock() {
state.locked = false state.locked = false
+2
View File
@@ -2,6 +2,7 @@
// 참조는 이름 기준(계좌명·분류명·태그명)으로 저장해 추후 '가져오기(복구)' 시 재매핑이 단순하도록 한다. // 참조는 이름 기준(계좌명·분류명·태그명)으로 저장해 추후 '가져오기(복구)' 시 재매핑이 단순하도록 한다.
import { Capacitor } from '@capacitor/core' import { Capacitor } from '@capacitor/core'
import { accountApi } from '@/api/accountApi' import { accountApi } from '@/api/accountApi'
import { appLock } from '@/composables/appLock'
async function safe(promise) { async function safe(promise) {
try { try {
@@ -25,6 +26,7 @@ async function saveWorkbook(XLSX, wb, name) {
const { Share } = await import('@capacitor/share') const { Share } = await import('@capacitor/share')
const res = await Filesystem.writeFile({ path: name, data: base64, directory: Directory.Cache }) const res = await Filesystem.writeFile({ path: name, data: base64, directory: Directory.Cache })
try { try {
appLock.suppressLock() // 공유 시트 복귀 시 앱 잠금 오작동 방지
await Share.share({ title: name, text: '가계부 백업 파일', url: res.uri }) await Share.share({ title: name, text: '가계부 백업 파일', url: res.uri })
} catch { } catch {
/* 공유 취소 — 파일은 캐시에 저장됨 */ /* 공유 취소 — 파일은 캐시에 저장됨 */
+2
View File
@@ -4,6 +4,7 @@ import { accountApi } from '@/api/accountApi'
import { reminders } from '@/native/reminders' import { reminders } from '@/native/reminders'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useDialog } from '@/composables/dialog' import { useDialog } from '@/composables/dialog'
import { appLock } from '@/composables/appLock'
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'
import { cardNotif } from '@/native/cardNotif' import { cardNotif } from '@/native/cardNotif'
@@ -136,6 +137,7 @@ async function chooseGallery() {
// 네이티브 전용 — 카메라 플러그인을 이 시점에만 동적 로드 // 네이티브 전용 — 카메라 플러그인을 이 시점에만 동적 로드
async function captureFrom(sourceKey) { async function captureFrom(sourceKey) {
try { try {
appLock.suppressLock() // 카메라 복귀 시 앱 잠금 오작동 방지
const { Camera, CameraResultType, CameraSource } = await import('@capacitor/camera') const { Camera, CameraResultType, CameraSource } = await import('@capacitor/camera')
const photo = await Camera.getPhoto({ const photo = await Camera.getPhoto({
source: sourceKey === 'CAMERA' ? CameraSource.Camera : CameraSource.Photos, source: sourceKey === 'CAMERA' ? CameraSource.Camera : CameraSource.Photos,