- @capacitor/local-notifications 추가, src/native/reminders.js - 매일 설정 시각(기본 21시) 가계부 미기록 시 알림(오늘 기록하면 오늘자 제외, 7일 롤링) - 구독 해지 상태 만료 3일 전 알림 - 설정에 알림 받기 토글 + 시간 선택(앱에서만), 권한 요청 - 앱 시작/홈/내역에서 스케줄 보정 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,17 +1,66 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
import { useDialog } from '@/composables/dialog'
|
||||
import { useUiStore } from '@/stores/ui'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { exportBackup, importBackup } from '@/utils/backup'
|
||||
import { reminders } from '@/native/reminders'
|
||||
|
||||
const dialog = useDialog()
|
||||
const ui = useUiStore()
|
||||
const auth = useAuthStore()
|
||||
const isPremium = computed(() => auth.isPremium) // 데이터 백업/복구는 유료 전용
|
||||
|
||||
// ===== 알림(로컬) 설정 — 앱에서만 =====
|
||||
const notifSupported = reminders.isNative()
|
||||
const notifEnabled = ref(false)
|
||||
const notifHour = ref(21)
|
||||
const notifSaving = ref(false)
|
||||
|
||||
function subFromUser() {
|
||||
return { premium: auth.isPremium, autoRenew: !!auth.user?.planAutoRenew, expiresAt: auth.user?.planExpiresAt }
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!notifSupported) return
|
||||
const s = await reminders.getSettings()
|
||||
notifEnabled.value = s.enabled
|
||||
notifHour.value = s.hour
|
||||
})
|
||||
|
||||
async function toggleNotif() {
|
||||
if (notifSaving.value) return
|
||||
const next = !notifEnabled.value
|
||||
notifSaving.value = true
|
||||
try {
|
||||
if (next) {
|
||||
const ok = await reminders.ensurePermission()
|
||||
if (!ok) {
|
||||
await dialog.alert('알림 권한이 꺼져 있어요. 기기 설정에서 알림을 허용해 주세요.')
|
||||
return
|
||||
}
|
||||
}
|
||||
notifEnabled.value = next
|
||||
await reminders.setSettings({ enabled: next, hour: notifHour.value })
|
||||
if (next) {
|
||||
await reminders.scheduleEntryReminder(false)
|
||||
await reminders.scheduleExpiryReminder(subFromUser())
|
||||
} else {
|
||||
await reminders.cancelAll()
|
||||
}
|
||||
} finally {
|
||||
notifSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function changeNotifHour(v) {
|
||||
notifHour.value = Number(v)
|
||||
await reminders.setSettings({ enabled: notifEnabled.value, hour: notifHour.value })
|
||||
if (notifEnabled.value) await reminders.scheduleEntryReminder(false)
|
||||
}
|
||||
|
||||
const exporting = ref(false)
|
||||
async function doExport() {
|
||||
if (exporting.value) return
|
||||
@@ -125,6 +174,30 @@ async function clearAppData() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 알림 (앱 전용 — 로컬 알림) -->
|
||||
<section v-if="notifSupported" class="card">
|
||||
<div class="row">
|
||||
<div class="row-main">
|
||||
<span class="row-label">알림 받기</span>
|
||||
<span class="row-sub">가계부 기록 리마인더 · 구독 만료 안내</span>
|
||||
</div>
|
||||
<button
|
||||
type="button" class="switch" :class="{ on: notifEnabled }"
|
||||
:disabled="notifSaving" role="switch" :aria-checked="notifEnabled"
|
||||
@click="toggleNotif"
|
||||
><span class="knob"></span></button>
|
||||
</div>
|
||||
<div v-if="notifEnabled" class="row">
|
||||
<div class="row-main">
|
||||
<span class="row-label">리마인더 시간</span>
|
||||
<span class="row-sub">오늘 기록이 없으면 이 시각에 알려드려요</span>
|
||||
</div>
|
||||
<select class="res-select" :value="notifHour" @change="changeNotifHour($event.target.value)">
|
||||
<option v-for="h in 24" :key="h - 1" :value="h - 1">{{ String(h - 1).padStart(2, '0') }}:00</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PC(데스크톱) 전용: 창 크기 (가로·세로 각각) -->
|
||||
<section v-if="isDesktop" class="card">
|
||||
<div class="row">
|
||||
@@ -312,6 +385,38 @@ async function clearAppData() {
|
||||
.hidden-file {
|
||||
display: none;
|
||||
}
|
||||
.switch {
|
||||
flex-shrink: 0;
|
||||
width: 48px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: #b0b0b0;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s;
|
||||
padding: 0;
|
||||
}
|
||||
.switch.on {
|
||||
background: hsla(160, 100%, 37%, 1);
|
||||
}
|
||||
.switch:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
.switch .knob {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
transition: transform 0.18s;
|
||||
}
|
||||
.switch.on .knob {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
.seg-btn {
|
||||
padding: 0.4rem 0.7rem;
|
||||
border: 0;
|
||||
|
||||
Reference in New Issue
Block a user