- 자체 구현(네이티브 플러그인 없음): PIN은 SHA-256 해시만 저장, 기본 OFF 옵트인 - 앱 시작(저장 세션 재진입)·백그라운드 복귀 시 잠금 화면 노출 - 설정에 앱 잠금 토글 + PIN 설정(입력→확인 2단계) 모달 - 갇힘 방지: 잠금 화면에서 'PIN 분실 → 로그아웃'(잠금 해제 후 로그아웃) - 신규/민감정보 앱 신뢰도 향상 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { appLock, PIN_LENGTH } from '@/composables/appLock'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import PinPad from '@/components/PinPad.vue'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const pin = ref('')
|
||||
const error = ref(false)
|
||||
|
||||
async function onComplete(p) {
|
||||
if (await appLock.verify(p)) {
|
||||
pin.value = ''
|
||||
error.value = false
|
||||
appLock.unlock()
|
||||
} else {
|
||||
error.value = true
|
||||
pin.value = ''
|
||||
}
|
||||
}
|
||||
function onInput(v) {
|
||||
pin.value = v
|
||||
error.value = false
|
||||
}
|
||||
async function forgot() {
|
||||
// PIN 분실 → 잠금 해제 + 로그아웃(재로그인 후 다시 설정). 갇힘 방지.
|
||||
appLock.disable()
|
||||
await auth.logout()
|
||||
appLock.unlock()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="lock-screen">
|
||||
<div class="lock-inner">
|
||||
<div class="lock-logo">🐷</div>
|
||||
<h1 class="lock-title">돈돼지 가계부</h1>
|
||||
<p class="lock-sub" :class="{ err: error }">
|
||||
{{ error ? 'PIN이 일치하지 않아요' : 'PIN을 입력하세요' }}
|
||||
</p>
|
||||
<PinPad :value="pin" :length="PIN_LENGTH" class="lock-pad" @update:value="onInput" @complete="onComplete" />
|
||||
<button type="button" class="lock-forgot" @click="forgot">PIN을 잊으셨나요? 로그아웃</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.lock-screen {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 3000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: calc(env(safe-area-inset-top, 0) + 1rem) 1rem calc(env(safe-area-inset-bottom, 0) + 1rem);
|
||||
background: var(--color-background);
|
||||
}
|
||||
.lock-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.lock-logo {
|
||||
font-size: 2.6rem;
|
||||
}
|
||||
.lock-title {
|
||||
margin-top: 0.4rem;
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
.lock-sub {
|
||||
margin-top: 0.6rem;
|
||||
margin-bottom: 1.8rem;
|
||||
font-size: 0.92rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.lock-sub.err {
|
||||
color: #c0392b;
|
||||
opacity: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
.lock-forgot {
|
||||
margin-top: 2rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
opacity: 0.6;
|
||||
font-size: 0.85rem;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
.lock-forgot:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup>
|
||||
// 재사용 PIN 키패드 — 점 표시 + 숫자 키패드. 길이 도달 시 complete 방출.
|
||||
const props = defineProps({
|
||||
value: { type: String, default: '' },
|
||||
length: { type: Number, default: 4 },
|
||||
})
|
||||
const emit = defineEmits(['update:value', 'complete'])
|
||||
|
||||
function press(d) {
|
||||
if (props.value.length >= props.length) return
|
||||
const next = props.value + d
|
||||
emit('update:value', next)
|
||||
if (next.length === props.length) emit('complete', next)
|
||||
}
|
||||
function del() {
|
||||
emit('update:value', props.value.slice(0, -1))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pinpad">
|
||||
<div class="dots">
|
||||
<span v-for="i in length" :key="i" class="dot" :class="{ on: value.length >= i }"></span>
|
||||
</div>
|
||||
<div class="keys">
|
||||
<button v-for="n in 9" :key="n" type="button" class="key" @click="press(String(n))">{{ n }}</button>
|
||||
<span class="key empty" aria-hidden="true"></span>
|
||||
<button type="button" class="key" @click="press('0')">0</button>
|
||||
<button type="button" class="key del" aria-label="지우기" @click="del">⌫</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pinpad {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.6rem;
|
||||
}
|
||||
.dots {
|
||||
display: flex;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
.dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid currentColor;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.dot.on {
|
||||
background: currentColor;
|
||||
opacity: 1;
|
||||
}
|
||||
.keys {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 64px);
|
||||
gap: 0.8rem;
|
||||
}
|
||||
.key {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background-soft);
|
||||
color: var(--color-text);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
.key:active {
|
||||
background: var(--color-border);
|
||||
}
|
||||
.key.empty {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
.key.del {
|
||||
font-size: 1.2rem;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user