feat(recurring): 고정지출 무료 2건 UI — 유료 잠금 해제·한도 안내·업셀
Deploy / deploy (push) Failing after 12m10s

라우터 requiresPremium 제거, 사이드바 🔒 제거, 자동실행을 전 사용자로,
RecurringView 한도 도달 시 추가 버튼 업셀 다이얼로그+힌트(→/upgrade).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb
2026-07-07 21:22:45 +09:00
parent c694dd9d04
commit 4d8881342a
4 changed files with 33 additions and 10 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ const icons = {
<RouterLink to="/account/recurrings" class="menu-item">
<svg class="menu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" v-html="icons.recurrings" />
<span>고정 지출</span>
<span v-if="lockPremium" class="lock-badge">🔒</span>
<!-- 무료도 2건까지 사용 가능 🔒 미표시 -->
</RouterLink>
<RouterLink to="/account/wallets" class="menu-item">
<svg class="menu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" v-html="icons.wallets" />
+2 -1
View File
@@ -155,7 +155,8 @@ const router = createRouter({
path: '/account/recurrings',
name: 'account-recurrings',
component: () => import('../views/account/RecurringView.vue'),
meta: { requiresAuth: true, requiresPremium: true },
// 고정지출은 무료도 2건까지 체험(초과 시 업셀) — 유료 게이트 해제
meta: { requiresAuth: true },
},
// 설정 (앱 하단 내비게이션 → 설정)
{
+5 -7
View File
@@ -941,13 +941,11 @@ async function analyzePasted() {
onMounted(async () => {
// 진입 시 밀린 정기 거래를 먼저 반영한 뒤 목록을 불러온다 (중복 없이)
// 고정지출은 유료 전용 — 무료 회원은 호출 자체를 건너뛴다(403/업그레이드 리다이렉트 방지)
if (isPremium.value) {
try {
await accountApi.runRecurrings()
} catch {
// 정기 거래 반영 실패는 가계부 조회를 막지 않는다
}
// 고정지출은 무료도 2건까지 사용 가능 → 모든 로그인 사용자에 대해 실행
try {
await accountApi.runRecurrings()
} catch {
// 정기 거래 반영 실패는 가계부 조회를 막지 않는다
}
load()
loadTagOptions()
+25 -1
View File
@@ -1,16 +1,26 @@
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { accountApi } from '@/api/accountApi'
import { useDialog } from '@/composables/dialog'
import { useAuthStore } from '@/stores/auth'
import IconBtn from '@/components/ui/IconBtn.vue'
import AppModal from '@/components/ui/AppModal.vue'
import CategoryPicker from '@/components/ui/CategoryPicker.vue'
import ChipSelect from '@/components/ui/ChipSelect.vue'
const dialog = useDialog()
const auth = useAuthStore()
const router = useRouter()
const recurrings = ref([])
const wallets = ref([])
// 무료 회원 고정지출 등록 한도(써보고 → 업셀). 유료는 무제한. 백엔드에서도 강제.
const FREE_RECURRING_LIMIT = 2
const atRecurringLimit = computed(
() => !auth.isPremium && recurrings.value.length >= FREE_RECURRING_LIMIT,
)
const categories = ref([])
const loading = ref(false)
const error = ref(null)
@@ -162,7 +172,15 @@ async function runNow() {
}
}
function openCreate() {
async function openCreate() {
if (atRecurringLimit.value) {
const go = await dialog.confirm(
`무료 회원은 고정 지출을 ${FREE_RECURRING_LIMIT}개까지 등록할 수 있어요.\n유료로 업그레이드하면 무제한이에요.`,
{ title: '등록 한도', okText: '업그레이드', cancelText: '닫기' },
)
if (go) router.push('/upgrade')
return
}
editId.value = null
Object.assign(form, {
title: '', type: 'EXPENSE', amount: null, category: '', memo: '',
@@ -245,6 +263,10 @@ onMounted(load)
<IconBtn icon="refresh" title="지금 반영" @click="runNow" />
<IconBtn icon="plus" title="고정 지출 추가" variant="primary" @click="openCreate" />
</div>
<p v-if="atRecurringLimit" class="msg recur-limit">
무료 회원은 고정 지출을 {{ FREE_RECURRING_LIMIT }}개까지 등록할 있어요 ·
<RouterLink to="/upgrade" class="recur-upsell">유료는 무제한 </RouterLink>
</p>
<p v-if="error" class="msg error">{{ error }}</p>
<p v-if="loading" class="msg">불러오는 중...</p>
@@ -475,6 +497,8 @@ button.primary { border-color: hsla(160, 100%, 37%, 1); color: hsla(160, 100%, 3
.actions button { padding: 0.2rem 0.55rem; font-size: 0.82rem; }
.msg { margin: 0.75rem 0; }
.msg.error { color: #c0392b; }
.msg.recur-limit { color: #b8860b; font-size: 0.85rem; }
.recur-upsell { color: hsla(160, 100%, 37%, 1); font-weight: 700; }
/* 폼 */
.recur-form {