- DemoView: 자체 메뉴로 섹션 전환, 4개 화면 샘플 데이터, 통계·예산·태그·커뮤니티는 잠금(로그인 유도) 안내 - 공개 라우트 /demo, 랜딩에 '로그인 없이 둘러보기' 버튼, 헤더 타이틀 '둘러보기' Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
<script setup>
|
||||
// 로그인 없이 둘러보기 — 자체 메뉴로 가계부/고정지출/계좌/분류를 샘플(더미) 데이터로 보여준다.
|
||||
// 나머지 메뉴는 로그인 유도 안내. API 호출/실데이터 없음.
|
||||
import { ref, computed } from 'vue'
|
||||
import { useUiStore } from '@/stores/ui'
|
||||
|
||||
const ui = useUiStore()
|
||||
|
||||
const MENU = [
|
||||
{ key: 'account', label: '가계부 내역' },
|
||||
{ key: 'stats', label: '통계', locked: true },
|
||||
{ key: 'recurrings', label: '고정 지출' },
|
||||
{ key: 'wallets', label: '계좌 관리' },
|
||||
{ key: 'categories', label: '분류 관리' },
|
||||
{ key: 'budget', label: '예산 설정', locked: true },
|
||||
{ key: 'tags', label: '태그 관리', locked: true },
|
||||
{ key: 'board', label: '커뮤니티', locked: true },
|
||||
]
|
||||
const active = ref('account')
|
||||
const activeItem = computed(() => MENU.find((m) => m.key === active.value) || MENU[0])
|
||||
|
||||
// ===== 더미 데이터 =====
|
||||
const summary = { income: 4600000, expense: 1827600, balance: 2772400 }
|
||||
const accountGroups = [
|
||||
{ date: '6월 27일 (금)', total: -42300, items: [
|
||||
{ cat: '식비', memo: '점심 · 김밥천국', type: 'EXPENSE', amount: 9000 },
|
||||
{ cat: '카페/간식', memo: '스타벅스', type: 'EXPENSE', amount: 5300 },
|
||||
{ cat: '교통', memo: '지하철', type: 'EXPENSE', amount: 2800 },
|
||||
{ cat: '쇼핑', memo: '다이소', type: 'EXPENSE', amount: 25200 },
|
||||
] },
|
||||
{ date: '6월 25일 (수)', total: 4554700, items: [
|
||||
{ cat: '급여', memo: '6월 급여', type: 'INCOME', amount: 4600000 },
|
||||
{ cat: '통신', memo: '휴대폰 요금', type: 'EXPENSE', amount: 45300 },
|
||||
] },
|
||||
{ date: '6월 23일 (월)', total: -68000, items: [
|
||||
{ cat: 'OTT/구독', memo: '넷플릭스', type: 'EXPENSE', amount: 17000 },
|
||||
{ cat: '식비', memo: '저녁 · 배달', type: 'EXPENSE', amount: 23000 },
|
||||
{ cat: '생활', memo: '마트 장보기', type: 'EXPENSE', amount: 28000 },
|
||||
] },
|
||||
]
|
||||
const recurrings = [
|
||||
{ title: '월세', freq: '매월 1일', cat: '주거', type: 'EXPENSE', amount: 600000 },
|
||||
{ title: '통신비', freq: '매월 15일', cat: '통신', type: 'EXPENSE', amount: 45300 },
|
||||
{ title: '넷플릭스', freq: '매월 23일', cat: 'OTT/구독', type: 'EXPENSE', amount: 17000 },
|
||||
{ title: '정기적금', freq: '매월 25일', cat: '저축', type: 'TRANSFER', amount: 300000 },
|
||||
{ title: '급여', freq: '매월 25일', cat: '급여', type: 'INCOME', amount: 4600000 },
|
||||
]
|
||||
const wallets = {
|
||||
total: 15520000,
|
||||
items: [
|
||||
{ name: '신한 주거래', kind: '은행', amount: 2540000 },
|
||||
{ name: '주택청약', kind: '은행', amount: 5000000 },
|
||||
{ name: '현금', kind: '현금', amount: 120000 },
|
||||
{ name: '삼성카드', kind: '카드', amount: -340000 },
|
||||
{ name: '연금저축(투자)', kind: '투자', amount: 8200000 },
|
||||
],
|
||||
}
|
||||
const categories = [
|
||||
{ major: '식비', subs: ['점심', '저녁', '카페/간식', '배달'] },
|
||||
{ major: '교통', subs: ['대중교통', '택시', '주유'] },
|
||||
{ major: '주거/통신', subs: ['월세', '관리비', '통신비'] },
|
||||
{ major: '문화/여가', subs: ['OTT/구독', '영화', '여행'] },
|
||||
{ major: '쇼핑', subs: ['생활', '의류', '온라인'] },
|
||||
]
|
||||
|
||||
function won(n) {
|
||||
return (n < 0 ? '-' : '') + Math.abs(n).toLocaleString('ko-KR')
|
||||
}
|
||||
function typeLabel(t) {
|
||||
return t === 'INCOME' ? '수입' : t === 'TRANSFER' ? '이체' : '지출'
|
||||
}
|
||||
function goLogin() {
|
||||
ui.openLogin('/account')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="demo">
|
||||
<div class="banner">
|
||||
<span>👀 <b>둘러보기 모드</b> · 아래는 샘플 데이터입니다.</span>
|
||||
<button type="button" class="login-btn" @click="goLogin">로그인하고 시작</button>
|
||||
</div>
|
||||
|
||||
<div class="demo-shell">
|
||||
<!-- 데모 메뉴 -->
|
||||
<nav class="demo-menu">
|
||||
<button
|
||||
v-for="m in MENU" :key="m.key" type="button"
|
||||
class="dm-item" :class="{ active: active === m.key }"
|
||||
@click="active = m.key"
|
||||
>
|
||||
<span>{{ m.label }}</span>
|
||||
<span v-if="m.locked" class="lock">🔒</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- 콘텐츠 -->
|
||||
<div class="demo-content">
|
||||
<!-- 가계부 내역 -->
|
||||
<template v-if="active === 'account'">
|
||||
<div class="month-nav"><span class="chev">‹</span><span class="period">2026년 6월</span><span class="chev">›</span></div>
|
||||
<div class="summary">
|
||||
<div class="s-card"><span class="s-label">수입</span><span class="s-val income">{{ won(summary.income) }}</span></div>
|
||||
<div class="s-card"><span class="s-label">지출</span><span class="s-val expense">{{ won(summary.expense) }}</span></div>
|
||||
<div class="s-card"><span class="s-label">잔액</span><span class="s-val">{{ won(summary.balance) }}</span></div>
|
||||
</div>
|
||||
<div v-for="g in accountGroups" :key="g.date" class="card-group">
|
||||
<div class="cg-head">
|
||||
<span class="cg-title">{{ g.date }}</span>
|
||||
<span class="cg-total" :class="g.total >= 0 ? 'income' : 'expense'">{{ won(g.total) }}</span>
|
||||
</div>
|
||||
<ul class="rows">
|
||||
<li v-for="(e, i) in g.items" :key="i" class="row3">
|
||||
<span class="tag">{{ e.cat }}</span>
|
||||
<span class="ellip">{{ e.memo }}</span>
|
||||
<span class="amt" :class="e.type === 'INCOME' ? 'income' : 'expense'">{{ e.type === 'INCOME' ? '+' : '-' }}{{ won(e.amount) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 고정 지출 -->
|
||||
<template v-else-if="active === 'recurrings'">
|
||||
<p class="hint">등록한 주기에 맞춰 가계부에 자동으로 '확인 필요' 내역이 생성됩니다.</p>
|
||||
<div class="card-group">
|
||||
<ul class="rows">
|
||||
<li v-for="(r, i) in recurrings" :key="i" class="row-rec">
|
||||
<div class="rec-main">
|
||||
<span class="rec-title">{{ r.title }}</span>
|
||||
<span class="rec-sub"><span class="tag">{{ r.cat }}</span> · {{ r.freq }} · {{ typeLabel(r.type) }}</span>
|
||||
</div>
|
||||
<span class="amt" :class="r.type === 'INCOME' ? 'income' : r.type === 'TRANSFER' ? '' : 'expense'">{{ won(r.amount) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 계좌 관리 -->
|
||||
<template v-else-if="active === 'wallets'">
|
||||
<div class="networth">
|
||||
<span class="nw-label">총자산</span>
|
||||
<span class="nw-val income">{{ won(wallets.total) }}</span>
|
||||
</div>
|
||||
<div class="card-group">
|
||||
<ul class="rows">
|
||||
<li v-for="(w, i) in wallets.items" :key="i" class="row-rec">
|
||||
<div class="rec-main">
|
||||
<span class="rec-title">{{ w.name }}</span>
|
||||
<span class="rec-sub"><span class="tag">{{ w.kind }}</span></span>
|
||||
</div>
|
||||
<span class="amt" :class="w.amount < 0 ? 'expense' : ''">{{ won(w.amount) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 분류 관리 -->
|
||||
<template v-else-if="active === 'categories'">
|
||||
<p class="hint">대분류 아래 소분류로 묶어 관리합니다. (예산·통계는 소분류 기준)</p>
|
||||
<div v-for="(c, i) in categories" :key="i" class="card-group">
|
||||
<div class="cg-head"><span class="major-badge">대</span><span class="cg-title">{{ c.major }}</span></div>
|
||||
<ul class="rows">
|
||||
<li v-for="(s, j) in c.subs" :key="j" class="row-sub2"><span class="sub-mark">↳</span>{{ s }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 잠금(로그인 유도) -->
|
||||
<div v-else class="locked-box">
|
||||
<div class="lock-big">🔒</div>
|
||||
<p class="lock-title">{{ activeItem.label }}는 로그인 후 이용할 수 있어요</p>
|
||||
<p class="lock-desc">로그인하면 통계·예산·태그·게시판까지 모두 사용할 수 있습니다.</p>
|
||||
<button type="button" class="cta-btn" @click="goLogin">로그인 / 시작하기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cta">
|
||||
<p>마음에 드시나요? 로그인하면 <b>내 가계부</b>를 바로 시작할 수 있어요.</p>
|
||||
<button type="button" class="cta-btn" @click="goLogin">로그인 / 시작하기</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.demo {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.7rem 0.9rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid hsla(160, 100%, 37%, 0.4);
|
||||
border-radius: 8px;
|
||||
background: hsla(160, 100%, 37%, 0.08);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.login-btn,
|
||||
.cta-btn {
|
||||
padding: 0.45rem 0.9rem;
|
||||
border: 1px solid hsla(160, 100%, 37%, 1);
|
||||
border-radius: 6px;
|
||||
background: hsla(160, 100%, 37%, 1);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.demo-shell {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.demo-menu {
|
||||
flex: none;
|
||||
width: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
padding: 0.4rem;
|
||||
background: var(--color-background-soft);
|
||||
}
|
||||
.dm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.3rem;
|
||||
padding: 0.55rem 0.6rem;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 0.9rem;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
.dm-item:hover {
|
||||
background: var(--color-background-mute);
|
||||
}
|
||||
.dm-item.active {
|
||||
background: hsla(160, 100%, 37%, 0.12);
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
font-weight: 600;
|
||||
}
|
||||
.lock {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.demo-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.month-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.chev {
|
||||
font-size: 1.4rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.period {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.6rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.s-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.7rem 0.8rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
background: var(--color-background-soft);
|
||||
}
|
||||
.s-label {
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.s-val {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.income {
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
}
|
||||
.expense {
|
||||
color: #c0392b;
|
||||
}
|
||||
.hint {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.networth {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
background: var(--color-background-soft);
|
||||
}
|
||||
.nw-label {
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.65;
|
||||
}
|
||||
.nw-val {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.card-group {
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cg-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
padding: 0.6rem 0.9rem;
|
||||
background: var(--color-background-mute);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.cg-title {
|
||||
font-weight: 600;
|
||||
margin-right: auto;
|
||||
}
|
||||
.cg-total {
|
||||
font-weight: 700;
|
||||
}
|
||||
.rows {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.row3 {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.row-rec {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.6rem;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.row-rec:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
.rec-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.rec-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.rec-sub {
|
||||
font-size: 0.78rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.row-sub2 {
|
||||
padding: 0.55rem 0.9rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.sub-mark {
|
||||
opacity: 0.4;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
.major-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
background: hsla(160, 100%, 37%, 0.15);
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
font-weight: 700;
|
||||
}
|
||||
.tag {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
background: var(--color-background-mute);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ellip {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.amt {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.locked-box {
|
||||
text-align: center;
|
||||
padding: 2.5rem 1rem;
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.lock-big {
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
.lock-title {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.lock-desc {
|
||||
margin: 0.4rem 0 1.1rem;
|
||||
font-size: 0.88rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.cta {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
.cta p {
|
||||
font-size: 0.92rem;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.cta .cta-btn {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
padding: 0.8rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-shell {
|
||||
flex-direction: column;
|
||||
}
|
||||
.demo-menu {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
.dm-item {
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useUiStore } from '@/stores/ui'
|
||||
import { accountApi } from '@/api/accountApi'
|
||||
@@ -8,6 +8,7 @@ import { ID_LOGIN_ENABLED } from '@/config/features'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const ui = useUiStore()
|
||||
const router = useRouter()
|
||||
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
@@ -298,6 +299,7 @@ onMounted(load)
|
||||
<div class="cta">
|
||||
<button type="button" class="btn primary" @click="ui.openLogin('/account')">로그인</button>
|
||||
<button v-if="ID_LOGIN_ENABLED && ui.signupEnabled" type="button" class="btn" @click="ui.openSignup()">회원가입</button>
|
||||
<button type="button" class="btn" @click="router.push('/demo')">로그인 없이 둘러보기</button>
|
||||
</div>
|
||||
<p class="cta-note">{{ !ID_LOGIN_ENABLED || ui.signupEnabled ? '로그인하면 나의 가계부 요약을 볼 수 있어요.' : '현재 회원가입이 제한되어 있습니다.' }}</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user