diff --git a/src/App.vue b/src/App.vue index 5d57b0e..2a7c525 100644 --- a/src/App.vue +++ b/src/App.vue @@ -9,6 +9,8 @@ import LoginModal from '@/components/LoginModal.vue' import SignupModal from '@/components/SignupModal.vue' import WebOnlyNotice from '@/components/WebOnlyNotice.vue' import AppDialog from '@/components/ui/AppDialog.vue' +import AppLockScreen from '@/components/AppLockScreen.vue' +import { appLock } from '@/composables/appLock' import { Capacitor } from '@capacitor/core' import { reminders } from '@/native/reminders' import { useAuthStore } from '@/stores/auth' @@ -71,6 +73,12 @@ function retryReload() { window.location.reload() } +// ===== 앱 잠금(PIN) ===== +// 백그라운드로 가면 잠가서, 복귀 시 잠금 화면이 떠 있도록 한다. +function onVisibility() { + if (document.hidden) appLock.lock() +} + onMounted(() => { if (!isApp) return // 웹은 안내만 — 앱 전용 초기화 생략 window.addEventListener('auth:unauthorized', onUnauthorized) @@ -78,6 +86,9 @@ onMounted(() => { window.addEventListener('online', onOnline) window.addEventListener('offline', onOffline) window.addEventListener('net:error', onNetError) + // 앱 잠금: 저장된 세션으로 재진입하는 경우 시작 시 잠금(새 로그인 직후는 잠그지 않음) + if (appLock.state.enabled && appLock.hasSession()) appLock.lock() + document.addEventListener('visibilitychange', onVisibility) ui.loadSignupEnabled() // 회원가입 허용 여부 선로딩(랜딩/로그인 가입 버튼에 반영) // 로그인 상태면 전체 프로필(아바타·포인트) 최신화 — 재로그인 없이 헤더 아바타 반영 if (auth.isAuthenticated) { @@ -99,6 +110,7 @@ onUnmounted(() => { window.removeEventListener('online', onOnline) window.removeEventListener('offline', onOffline) window.removeEventListener('net:error', onNetError) + document.removeEventListener('visibilitychange', onVisibility) clearTimeout(netTimer) }) @@ -115,6 +127,9 @@ watch(() => route.fullPath, () => ui.closeSidebar()) @@ -303,6 +374,53 @@ async function clearAppData() { max-width: 560px; margin: 0 auto; } +/* PIN 설정 모달 */ +.pin-backdrop { + position: fixed; + inset: 0; + z-index: 2500; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background: rgba(0, 0, 0, 0.5); +} +.pin-modal { + position: relative; + width: 100%; + max-width: 340px; + padding: 1.6rem 1.2rem 1.8rem; + border-radius: 14px; + background: var(--color-background); + border: 1px solid var(--color-border); + text-align: center; +} +.pin-close { + position: absolute; + top: 0.5rem; + right: 0.6rem; + width: 30px; + height: 30px; + border: 0; + background: transparent; + color: var(--color-text); + font-size: 1.4rem; + cursor: pointer; +} +.pin-h { + font-size: 1.1rem; + color: var(--color-heading); +} +.pin-sub { + margin: 0.4rem 0 1.4rem; + font-size: 0.85rem; + opacity: 0.7; +} +.pin-sub.err { + color: #c0392b; + opacity: 1; + font-weight: 600; +} .page-title { font-size: 1.4rem; margin-bottom: 1rem;