Files
sb-front/src/stores/auth.js
T
ByungCheol 9861ba90ae
CI / build (push) Failing after 15m5s
feat: 멤버십(무료/유료) 게이팅 — 프론트
- auth 스토어 isPremium/isAdmin 추가 (plan===PREMIUM 또는 관리자)
- 사이드바: 통계·고정지출·예산·태그 메뉴에 무료 회원 잠금 배지(🔒)
- 라우트 가드: 유료 전용 화면(requiresPremium) → 무료 회원은 /upgrade
- UpgradeView: 무료/유료 기능 비교 + 업그레이드 안내
- http 인터셉터: 403 PREMIUM_REQUIRED → premium:required 이벤트 → 업그레이드 화면
- 무료 화면의 유료 API 호출 차단(홈 예산·내역 태그·OCR·카드알림 게이팅)
- 회원관리: 멤버십 컬럼 + 무료/유료 토글(관리자)
- 설정: 데이터 백업/복구 유료 게이팅, 계정정보에 멤버십 표시

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 11:02:44 +09:00

125 lines
4.3 KiB
JavaScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { Preferences } from '@capacitor/preferences'
import { authApi } from '@/api/authApi'
import { exitDemo } from '@/demo'
// 세션 토큰은 Capacitor Preferences(네이티브 영속 저장 / 웹은 localStorage)로 보관한다.
// http.js 요청 인터셉터가 동기로 읽을 수 있도록 localStorage 에도 미러링한다.
const TOKEN_KEY = 'token'
const USER_KEY = 'auth_user'
export const useAuthStore = defineStore('auth', () => {
const token = ref('')
const user = ref(null)
const ready = ref(false)
const isAuthenticated = computed(() => !!token.value)
const isAdmin = computed(() => user.value?.role === 'ADMIN')
// 유료(PREMIUM) 멤버십 여부. 관리자는 항상 유료 기능 사용 가능.
const isPremium = computed(() => isAdmin.value || user.value?.plan === 'PREMIUM')
// localStorage 동기화 (http.js 가 동기로 토큰을 읽음)
function mirrorLocal() {
if (token.value) localStorage.setItem(TOKEN_KEY, token.value)
else localStorage.removeItem(TOKEN_KEY)
if (user.value) localStorage.setItem(USER_KEY, JSON.stringify(user.value))
else localStorage.removeItem(USER_KEY)
}
async function persist() {
// localStorage 미러는 항상 먼저(동기) — Preferences 실패해도 토큰 유실 없음
mirrorLocal()
try {
if (token.value) await Preferences.set({ key: TOKEN_KEY, value: token.value })
else await Preferences.remove({ key: TOKEN_KEY })
if (user.value) await Preferences.set({ key: USER_KEY, value: JSON.stringify(user.value) })
else await Preferences.remove({ key: USER_KEY })
} catch {
// Preferences 사용 불가(일부 모바일 웹/시크릿 모드) — localStorage 미러로 충분
}
}
// 앱/웹 시작 시 저장된 세션 복원 (자동 로그인). main.js 에서 mount 전 await.
async function restore() {
// 1) localStorage 우선 — 웹에서 항상 동작하고 http.js 의 토큰 소스와 동일.
// (Preferences.get 가 모바일 웹에서 throw 해도 토큰을 잃지 않도록 먼저 읽는다)
let t = localStorage.getItem(TOKEN_KEY) || ''
let uRaw = localStorage.getItem(USER_KEY)
// 2) 네이티브(Preferences)에만 있고 localStorage 에 없으면 보강. 실패해도 무시.
try {
if (!t) t = (await Preferences.get({ key: TOKEN_KEY })).value || ''
if (!uRaw) uRaw = (await Preferences.get({ key: USER_KEY })).value || null
} catch {
// Preferences 사용 불가(일부 모바일 웹/시크릿 모드) — localStorage 값으로 진행
}
token.value = t
user.value = safeParse(uRaw)
mirrorLocal()
ready.value = true
}
async function login(loginId, password, rememberMe = false) {
const res = await authApi.login({ loginId, password, rememberMe })
token.value = res.token
user.value = res.member
exitDemo()
await persist()
return res
}
// 구글 로그인 — GIS ID 토큰으로 세션 발급
async function googleLogin(idToken, rememberMe = true) {
const res = await authApi.googleLogin({ idToken, rememberMe })
token.value = res.token
user.value = res.member
exitDemo()
await persist()
return res
}
async function signup(payload) {
return authApi.signup(payload)
}
// 서버 세션 기준으로 현재 사용자 정보 동기화 (토큰 유효성 확인 용도)
async function fetchMe() {
const me = await authApi.me()
user.value = { ...(user.value || {}), ...me }
await persist()
return me
}
async function logout() {
try {
await authApi.logout()
} catch {
// 서버 세션이 이미 만료됐어도 클라이언트는 정리
}
await clear()
}
// 가입정보 변경 등으로 바뀐 사용자 필드를 로컬 세션에 병합 (헤더 이름 등 즉시 반영)
async function applyUser(partial) {
user.value = { ...(user.value || {}), ...partial }
await persist()
}
async function clear() {
exitDemo()
token.value = ''
user.value = null
await persist()
}
return { token, user, ready, isAuthenticated, isAdmin, isPremium, restore, login, googleLogin, signup, fetchMe, applyUser, logout, clear }
})
function safeParse(value) {
try {
return value ? JSON.parse(value) : null
} catch {
return null
}
}