- 자체 구현(네이티브 플러그인 없음): PIN은 SHA-256 해시만 저장, 기본 OFF 옵트인 - 앱 시작(저장 세션 재진입)·백그라운드 복귀 시 잠금 화면 노출 - 설정에 앱 잠금 토글 + PIN 설정(입력→확인 2단계) 모달 - 갇힘 방지: 잠금 화면에서 'PIN 분실 → 로그아웃'(잠금 해제 후 로그아웃) - 신규/민감정보 앱 신뢰도 향상 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,8 @@ import { useUiStore } from '@/stores/ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { exportBackup, importBackup } from '@/utils/backup'
|
||||
import { reminders } from '@/native/reminders'
|
||||
import { appLock, PIN_LENGTH } from '@/composables/appLock'
|
||||
import PinPad from '@/components/PinPad.vue'
|
||||
|
||||
const dialog = useDialog()
|
||||
const ui = useUiStore()
|
||||
@@ -61,6 +63,46 @@ async function changeNotifHour(v) {
|
||||
if (notifEnabled.value) await reminders.scheduleEntryReminder(false)
|
||||
}
|
||||
|
||||
// ===== 앱 잠금(PIN) =====
|
||||
const lockEnabled = computed(() => appLock.state.enabled)
|
||||
const lockSetupOpen = ref(false)
|
||||
const setupStep = ref(1) // 1: 입력, 2: 확인
|
||||
const setupPin = ref('')
|
||||
const setupFirst = ref('')
|
||||
const setupError = ref('')
|
||||
|
||||
function toggleLock() {
|
||||
if (lockEnabled.value) confirmDisableLock()
|
||||
else openLockSetup()
|
||||
}
|
||||
function openLockSetup() {
|
||||
setupStep.value = 1
|
||||
setupPin.value = ''
|
||||
setupFirst.value = ''
|
||||
setupError.value = ''
|
||||
lockSetupOpen.value = true
|
||||
}
|
||||
async function onSetupComplete(p) {
|
||||
if (setupStep.value === 1) {
|
||||
setupFirst.value = p
|
||||
setupPin.value = ''
|
||||
setupStep.value = 2
|
||||
setupError.value = ''
|
||||
} else if (p === setupFirst.value) {
|
||||
await appLock.setPin(p)
|
||||
lockSetupOpen.value = false
|
||||
} else {
|
||||
setupError.value = 'PIN이 일치하지 않아요. 처음부터 다시 설정해주세요.'
|
||||
setupStep.value = 1
|
||||
setupPin.value = ''
|
||||
setupFirst.value = ''
|
||||
}
|
||||
}
|
||||
async function confirmDisableLock() {
|
||||
const ok = await dialog.confirm('앱 잠금을 끌까요? 다음 실행부터 PIN 없이 열립니다.', { title: '앱 잠금 해제' })
|
||||
if (ok) appLock.disable()
|
||||
}
|
||||
|
||||
const exporting = ref(false)
|
||||
async function doExport() {
|
||||
if (exporting.value) return
|
||||
@@ -198,6 +240,21 @@ async function clearAppData() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 앱 잠금 (PIN) -->
|
||||
<section class="card">
|
||||
<div class="row">
|
||||
<div class="row-main">
|
||||
<span class="row-label">앱 잠금 (PIN)</span>
|
||||
<span class="row-sub">앱 실행·복귀 시 {{ PIN_LENGTH }}자리 PIN 입력</span>
|
||||
</div>
|
||||
<button
|
||||
type="button" class="switch" :class="{ on: lockEnabled }"
|
||||
role="switch" :aria-checked="lockEnabled"
|
||||
@click="toggleLock"
|
||||
><span class="knob"></span></button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PC(데스크톱) 전용: 창 크기 (가로·세로 각각) -->
|
||||
<section v-if="isDesktop" class="card">
|
||||
<div class="row">
|
||||
@@ -295,6 +352,20 @@ async function clearAppData() {
|
||||
</section>
|
||||
|
||||
<p class="copyright">© 2026 돈돼지 가계부. All rights reserved.</p>
|
||||
|
||||
<!-- PIN 설정 모달 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="lockSetupOpen" class="pin-backdrop" @click.self="lockSetupOpen = false">
|
||||
<div class="pin-modal" role="dialog" aria-modal="true" aria-label="PIN 설정">
|
||||
<button type="button" class="pin-close" aria-label="닫기" @click="lockSetupOpen = false">×</button>
|
||||
<h2 class="pin-h">{{ setupStep === 1 ? 'PIN 설정' : 'PIN 다시 입력' }}</h2>
|
||||
<p class="pin-sub" :class="{ err: setupError }">
|
||||
{{ setupError || (setupStep === 1 ? `사용할 ${PIN_LENGTH}자리 PIN을 입력하세요` : '확인을 위해 한 번 더 입력하세요') }}
|
||||
</p>
|
||||
<PinPad :value="setupPin" :length="PIN_LENGTH" @update:value="(v) => { setupPin = v; setupError = '' }" @complete="onSetupComplete" />
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user