feat: 멤버십(무료/유료) 게이팅 — 프론트
CI / build (push) Failing after 15m5s

- 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>
This commit is contained in:
ByungCheol
2026-06-28 11:02:44 +09:00
parent 2755d118c2
commit 9861ba90ae
12 changed files with 309 additions and 32 deletions
+11 -2
View File
@@ -1,6 +1,7 @@
<script setup>
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { accountApi } from '@/api/accountApi'
import { useAuthStore } from '@/stores/auth'
import { useDialog } from '@/composables/dialog'
import IconBtn from '@/components/ui/IconBtn.vue'
import { imageToBlob, parseReceiptText } from '@/utils/receiptOcr'
@@ -9,6 +10,8 @@ import { Capacitor } from '@capacitor/core'
// @capacitor/camera 는 네이티브에서만 동적 로드 (웹은 file input 사용 — 정적 import 시 모바일 웹 청크 로드 실패 유발)
const dialog = useDialog()
const auth = useAuthStore()
const isPremium = computed(() => auth.isPremium) // 태그·OCR 등 유료 기능 노출 제어
const now = new Date()
const year = ref(now.getFullYear())
const month = ref(now.getMonth() + 1)
@@ -329,6 +332,11 @@ function toggleTag(id) {
}
async function loadTagOptions() {
// 태그는 유료 전용 — 무료 회원은 호출하지 않음(403/업그레이드 리다이렉트 방지)
if (!isPremium.value) {
tagOptions.value = []
return
}
try {
tagOptions.value = await accountApi.tags()
} catch {
@@ -832,7 +840,7 @@ onMounted(async () => {
</div>
<!-- 카드 결제 알림 자동인식 권한 안내 (네이티브, 미허용 ) -->
<div v-if="notifNative && !notifEnabled" class="notif-banner">
<div v-if="isPremium && notifNative && !notifEnabled" class="notif-banner">
<span>💳 카드 결제 알림을 자동으로 가계부에 등록하려면 <b>알림 접근 권한</b> 필요합니다.</span>
<button type="button" class="notif-btn" @click="openNotifSettings">설정 열기</button>
</div>
@@ -959,6 +967,7 @@ onMounted(async () => {
/>
<div class="rb-actions">
<button
v-if="isPremium"
type="button" class="receipt-btn"
:disabled="submitting || ocrRunning" @click="pickReceipt"
>📷 영수증 스캔</button>
@@ -1074,7 +1083,7 @@ onMounted(async () => {
<label v-if="!isRepayment">금액<input v-model.number="form.amount" type="number" min="0" placeholder="원" :disabled="submitting" /></label>
<label>메모<input v-model="form.memo" type="text" placeholder="(선택)" :disabled="submitting" /></label>
<div v-if="!isRepayment" class="tag-field">
<div v-if="!isRepayment && isPremium" class="tag-field">
<span class="tag-label">태그</span>
<div class="tag-badges">
<button