- @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:
@@ -0,0 +1,124 @@
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
|
||||
// 로컬 알림(앱 전용) — 가계부 기록 리마인더 + 구독 만료 임박 알림.
|
||||
// 서버 푸시(FCM) 없이 기기에서 예약되며, 앱을 켜지 않아도 예약 시간에 표시된다.
|
||||
|
||||
const PREF_ENABLED = 'notif_enabled'
|
||||
const PREF_HOUR = 'notif_hour'
|
||||
const ENTRY_BASE = 5000 // 가계부 리마인더 id 5000~5006 (7일 롤링)
|
||||
const ENTRY_DAYS = 7
|
||||
const EXPIRY_ID = 5100 // 구독 만료 임박 id
|
||||
|
||||
let LN = null
|
||||
async function ln() {
|
||||
if (!Capacitor.isNativePlatform()) return null
|
||||
if (!LN) LN = (await import('@capacitor/local-notifications')).LocalNotifications
|
||||
return LN
|
||||
}
|
||||
|
||||
async function cancelIds(ids) {
|
||||
const n = await ln()
|
||||
if (!n) return
|
||||
try {
|
||||
await n.cancel({ notifications: ids.map((id) => ({ id })) })
|
||||
} catch {
|
||||
/* 무시 */
|
||||
}
|
||||
}
|
||||
|
||||
export const reminders = {
|
||||
isNative() {
|
||||
return Capacitor.isNativePlatform()
|
||||
},
|
||||
|
||||
async getSettings() {
|
||||
const e = (await Preferences.get({ key: PREF_ENABLED })).value
|
||||
const h = (await Preferences.get({ key: PREF_HOUR })).value
|
||||
return { enabled: e === '1', hour: h != null ? Number(h) : 21 }
|
||||
},
|
||||
|
||||
async setSettings({ enabled, hour }) {
|
||||
await Preferences.set({ key: PREF_ENABLED, value: enabled ? '1' : '0' })
|
||||
if (hour != null) await Preferences.set({ key: PREF_HOUR, value: String(hour) })
|
||||
},
|
||||
|
||||
/** 알림 권한 요청 (Android 13+). 허용되면 true */
|
||||
async ensurePermission() {
|
||||
const n = await ln()
|
||||
if (!n) return false
|
||||
let p = await n.checkPermissions()
|
||||
if (p.display !== 'granted') p = await n.requestPermissions()
|
||||
return p.display === 'granted'
|
||||
},
|
||||
|
||||
/** 가계부 리마인더 — 향후 7일 매일 예약(오늘 이미 기록했으면 오늘은 제외) */
|
||||
async scheduleEntryReminder(hasEntryToday = false) {
|
||||
const n = await ln()
|
||||
if (!n) return
|
||||
await cancelIds(Array.from({ length: ENTRY_DAYS }, (_, i) => ENTRY_BASE + i))
|
||||
const { enabled, hour } = await this.getSettings()
|
||||
if (!enabled) return
|
||||
const now = new Date()
|
||||
const list = []
|
||||
for (let i = 0; i < ENTRY_DAYS; i++) {
|
||||
const at = new Date(now)
|
||||
at.setDate(now.getDate() + i)
|
||||
at.setHours(hour, 0, 0, 0)
|
||||
if (at <= now) continue
|
||||
if (i === 0 && hasEntryToday) continue
|
||||
list.push({
|
||||
id: ENTRY_BASE + i,
|
||||
title: '돈돼지 가계부',
|
||||
body: '오늘 가계부 기록을 잊지 않으셨나요? 📝',
|
||||
schedule: { at },
|
||||
})
|
||||
}
|
||||
if (list.length) {
|
||||
try {
|
||||
await n.schedule({ notifications: list })
|
||||
} catch {
|
||||
/* 무시 */
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** 오늘 기록을 완료하면 오늘자 리마인더만 취소 */
|
||||
async clearTodayReminder() {
|
||||
await cancelIds([ENTRY_BASE])
|
||||
},
|
||||
|
||||
/**
|
||||
* 구독 만료 임박 알림 — 해지(자동갱신 off) 상태에서 만료 3일 전 예약.
|
||||
* sub: { premium, autoRenew, expiresAt }
|
||||
*/
|
||||
async scheduleExpiryReminder(sub) {
|
||||
const n = await ln()
|
||||
if (!n) return
|
||||
await cancelIds([EXPIRY_ID])
|
||||
const { enabled } = await this.getSettings()
|
||||
if (!enabled || !sub || !sub.premium || sub.autoRenew || !sub.expiresAt) return
|
||||
const exp = new Date(sub.expiresAt)
|
||||
if (Number.isNaN(exp.getTime())) return
|
||||
const at = new Date(exp.getTime() - 3 * 24 * 60 * 60 * 1000)
|
||||
at.setHours(10, 0, 0, 0)
|
||||
if (at <= new Date()) return
|
||||
try {
|
||||
await n.schedule({
|
||||
notifications: [{
|
||||
id: EXPIRY_ID,
|
||||
title: '돈돼지 가계부',
|
||||
body: `프리미엄 구독이 ${exp.getFullYear()}.${exp.getMonth() + 1}.${exp.getDate()}에 만료돼요. 계속 이용하려면 갱신해 주세요.`,
|
||||
schedule: { at },
|
||||
}],
|
||||
})
|
||||
} catch {
|
||||
/* 무시 */
|
||||
}
|
||||
},
|
||||
|
||||
/** 알림 전체 해제 (토글 off) */
|
||||
async cancelAll() {
|
||||
await cancelIds([...Array.from({ length: ENTRY_DAYS }, (_, i) => ENTRY_BASE + i), EXPIRY_ID])
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user