Merge branch 'dev'
Deploy / deploy (push) Failing after 13m37s
CI / build (push) Failing after 15m5s

This commit is contained in:
ByungCheol
2026-06-28 00:44:57 +09:00
5 changed files with 535 additions and 2 deletions
+55 -1
View File
@@ -156,13 +156,67 @@
--- ---
# (이어진 작업 4) 구글 로그인 · 다크모드 · UI 개편 (2026-06-28)
## 34. 구글 로그인/가입 (소셜 로그인)
- **웹/PC/앱 3플랫폼 구글 로그인**: 웹·PC(Electron)는 **Google Identity Services(GIS)** ID 토큰, 앱(안드로이드)은 **네이티브 플러그인 `@capawesome/capacitor-google-sign-in`**(Credential Manager). 모두 ID 토큰을 백엔드 `POST /api/auth/google` 로 전달 → **tokeninfo 로 검증(aud=웹 클라이언트 ID)** 후 세션 발급.
- **PC(Electron) 팝업 수정**: `accounts.google.com` 팝업을 외부 브라우저로 보내 결과가 안 돌아오던 문제 → **앱 내부 자식 창**으로 열어 opener 로 credential 전달.
- **같은 이메일 자동 연결**: 구글 로그인 시 `member.google_id`(신규 컬럼)로 조회, 없으면 **검증된 동일 이메일의 기존 계정에 연결**(provider 유지 → 아이디 로그인/프로필 편집 보존), 그래도 없으면 신규 생성.
- 설정 필요: Google Cloud **웹 + Android OAuth 클라이언트**(패키지 `kr.sblog.slimbudget` + 서명 SHA-1), 서버 `.env` `GOOGLE_CLIENT_ID`.
## 35. 구글 전용 로그인 (아이디 로그인/회원가입 숨김)
- `src/config/features.js` `ID_LOGIN_ENABLED=false` 플래그로 **아이디/비번 폼·회원가입 UI 숨김**(로그인 모달·홈 랜딩). 백엔드 `/auth/login`·`/signup` 은 비상용으로 유지(복구 가능).
## 36. 브랜딩 — 돈돼지 가계부
- 앱/파비콘/PC 아이콘을 **돈돼지(투명 배경)** 로 교체, 헤더 로고·런처 라벨 정리. 런처 라벨 `돈돼지가계부`(한 줄), 앱 표시명 `돈돼지 가계부`.
## 37. PC 설치파일 (Electron)
- 산출물명 `SlimBudget-Setup.exe`**`DonDwaeji-Setup.exe`**, 다운로드 URL `/download/DonDwaeji-Setup.exe`.
- **사용자 폴더 설치**(`%LOCALAPPDATA%\Programs\DonDwaeji`, `build/installer.nsh`)로 변경 → **관리자 권한(UAC) 프롬프트 제거**. (게시자 미서명 경고/SmartScreen 은 인증서 없이는 잔존)
## 38. 게시판 공지 기능 (관리자)
- `post.notice` 컬럼(멱등 ALTER), 목록 **정렬 `notice DESC, id DESC`(공지 최상단)**, 목록·상세 **공지 배지**.
- 공지 설정은 **글 작성/수정 시 체크박스**로 일원화(커뮤니티+관리자). 상세의 공지 토글 버튼 제거, **목록 이동 아이콘** 추가.
## 39. 라이트/다크 테마
- 설정 화면에 **시스템 / 라이트 / 다크** 선택. `base.css`**`data-theme` 기반 수동 다크 + 시스템 폴백**으로 전환, `theme.js` 유틸 + ui 스토어 상태, `main.js` 시작 시 즉시 적용(깜빡임 방지).
## 40. 사이드바/헤더 개편 · 레이아웃 정리
- 사이드바 상단에 **돼지 로고 + '돈돼지 가계부'**(메뉴 텍스트 대체), 사이드바 **메뉴 아이콘**(lucide) 추가. **헤더는 현재 화면 타이틀** 표시(돼지 로고 제거).
- **PC 레이아웃**: 그리드 `'left top'` → 사이드바가 **좌측 상단(헤더 행)까지** 차지, 헤더는 우측 콘텐츠 영역만.
- **화면 내 페이지 타이틀(`<h1>`) 제거 → 헤더로 일원화**, **화면 내 뒤로가기 버튼 제거**(하단 내비 뒤로가기만 유지).
- **전 화면 본문 폭 통일**: `--page-max`(760px) + `.layout-body > *` 전역 규칙(뷰별 제각각 max-width 대체).
## 41. 화면별 액션 아이콘 위치 정리
- 가계부/예산 **month-nav**: 좌 spacer + 우 액션으로 **월 변경은 가운데, 추가·검색/필터는 우측**. 고정지출: 안내문구 아래. 커뮤니티 글쓰기: 태그 필터와 같은 줄. 분류: 지출/수입 탭과 같은 줄.
- **커뮤니티 검색 모달**: input `min-width:0` 으로 **검색 버튼이 모달 밖으로 밀리던 문제** 수정.
## 42. 자주 쓰는 내역 UX 정리
- 등록 모달의 **'자주 쓰는 내역 불러오기' 버튼·피커 모달 제거**(번거로운 3단계 해소, 저장 기능은 유지). 가계부 quick-bar 에 **'⭐ 자주 쓰는 내역' 라벨** 추가.
- 빠른등록(`useQuick`) **등록 성공과 목록 갱신 분리** — 갱신 오류가 '등록 실패'로 오인되던 케이스 방지.
## 43. 계정정보 단순화 · 구글 계정 이름 변경
- 계정정보에서 **아이디·가입유형 행 제거**(이름/이메일/권한만). **소셜(구글) 계정도 비밀번호 재인증 없이 이름만 변경** 가능(ProfileEdit), 백엔드 `updateProfile` 그대로.
## 44. PC 화면 해상도 변경
- Electron **preload(`window.desktop`) + IPC(`window:setSize`)** 추가, 설정 화면(PC)에 **창 크기 프리셋**(1024×720 ~ 1920×1080). 선택 크기는 Electron 창 상태 저장으로 다음 실행에도 유지.
## 45. 기타
- 통계 '분류별 지출/수입'을 **대분류 기준 집계**(소분류 펼침 제거로 스크롤 단축).
- 게시판 작성일: 1시간 이내 'N분 전' / 1일 이내 'N시간 전' / 그 이상 **`YYYY.MM.DD`**.
- 웹 안내 페이지 한국어 줄바꿈 **절(어절) 단위**(`word-break: keep-all` + inline-block 묶음).
---
## 운영 반영 시 체크 ## 운영 반영 시 체크
- 서버 `/opt/sb-backend/.env` - 서버 `/opt/sb-backend/.env`
- `ACCOUNT_CRYPTO_KEY` — 계좌번호 암호화를 켤 때만(미설정 시 평문, 깨지지 않음). - `ACCOUNT_CRYPTO_KEY` — 계좌번호 암호화를 켤 때만(미설정 시 평문, 깨지지 않음).
- `GOOGLE_CLIENT_ID` — 구글 로그인 웹 클라이언트 ID(미설정 시 구글 버튼 미노출). **앱 구글 로그인**은 Google Cloud 에 **Android OAuth 클라이언트**(패키지 `kr.sblog.slimbudget` + 서명 SHA-1) 등록 필요.
- `member.google_id`(계정 연결) · `post.notice`(공지) 컬럼 — 기동 시 **멱등 ALTER** 자동 적용.
- `app_setting` · `auth_session` · `board_image` 테이블 — 배포 시 `member.sql`/`board.sql` 초기화로 자동 생성(가입 허용 / 세션 DB 백업 / 게시글 이미지). - `app_setting` · `auth_session` · `board_image` 테이블 — 배포 시 `member.sql`/`board.sql` 초기화로 자동 생성(가입 허용 / 세션 DB 백업 / 게시글 이미지).
- Redis `appendonly yes` — 세션 영속(이미 적용). 단, 세션 DB 이중 저장으로 Redis 유실에도 로그인 유지. - Redis `appendonly yes` — 세션 영속(이미 적용). 단, 세션 DB 이중 저장으로 Redis 유실에도 로그인 유지.
- 서버 `.env`/환경변수 `PUBLIC_URL`(기본 `https://app.sblog.kr`) — 업로드 이미지 절대 URL 베이스(앱에서 이미지 로드에 필요). - 서버 `.env`/환경변수 `PUBLIC_URL`(기본 `https://app.sblog.kr`) — 업로드 이미지 절대 URL 베이스(앱에서 이미지 로드에 필요).
- **nginx**(`deploy/nginx-sb-front.conf` 참고): `location /``Cache-Control no-cache`(진입 HTML 최신화), `location /download/``/var/www/sb-downloads`(설치파일, 배포에 안 지워지는 영속 경로). - **nginx**(`deploy/nginx-sb-front.conf` 참고): `location /``Cache-Control no-cache`(진입 HTML 최신화), `location /download/``/var/www/sb-downloads`(설치파일, 배포에 안 지워지는 영속 경로).
- 앱 버전 표기는 `package.json``version`(현재 `1.0.0`)을 단일 출처로 사용. - 앱 버전 표기는 `package.json``version`(현재 `1.0.0`)을 단일 출처로 사용.
- **PC 설치파일**: `npm run electron:build``release-desktop/SlimBudget-Setup.exe`(서명 없음, SmartScreen 경고 정상). - **PC 설치파일**: `npm run electron:build``release-desktop/DonDwaeji-Setup.exe`(**사용자 폴더 설치=UAC 없음**, 미서명 → SmartScreen 경고 정상). 서버는 `/var/www/sb-downloads/DonDwaeji-Setup.exe` 로 업로드.
- 배포는 `main` 푸시 → CI·Deploy 워크플로 모두 **테스트 통과해야 진행**(Gitea Actions). - 배포는 `main` 푸시 → CI·Deploy 워크플로 모두 **테스트 통과해야 진행**(Gitea Actions).
+1
View File
@@ -14,6 +14,7 @@ const route = useRoute()
// 라우트별 헤더 타이틀(현재 화면 이름) // 라우트별 헤더 타이틀(현재 화면 이름)
const TITLES = { const TITLES = {
home: '돈돼지 가계부', home: '돈돼지 가계부',
demo: '둘러보기',
'account-entries': '가계부 내역', 'account-entries': '가계부 내역',
'account-stats': '통계', 'account-stats': '통계',
'account-recurrings': '고정 지출', 'account-recurrings': '고정 지출',
+6
View File
@@ -14,6 +14,12 @@ const router = createRouter({
name: 'home', name: 'home',
component: HomeView, component: HomeView,
}, },
{
// 로그인 없이 둘러보기 (더미 데이터) — 공개
path: '/demo',
name: 'demo',
component: () => import('../views/DemoView.vue'),
},
{ {
path: '/users', path: '/users',
name: 'users', name: 'users',
+470
View File
@@ -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>
+3 -1
View File
@@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref, computed, onMounted, watch } from 'vue' import { ref, computed, onMounted, watch } from 'vue'
import { RouterLink } from 'vue-router' import { RouterLink, useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useUiStore } from '@/stores/ui' import { useUiStore } from '@/stores/ui'
import { accountApi } from '@/api/accountApi' import { accountApi } from '@/api/accountApi'
@@ -8,6 +8,7 @@ import { ID_LOGIN_ENABLED } from '@/config/features'
const auth = useAuthStore() const auth = useAuthStore()
const ui = useUiStore() const ui = useUiStore()
const router = useRouter()
const now = new Date() const now = new Date()
const year = now.getFullYear() const year = now.getFullYear()
@@ -298,6 +299,7 @@ onMounted(load)
<div class="cta"> <div class="cta">
<button type="button" class="btn primary" @click="ui.openLogin('/account')">로그인</button> <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 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> </div>
<p class="cta-note">{{ !ID_LOGIN_ENABLED || ui.signupEnabled ? '로그인하면 나의 가계부 요약을 볼 수 있어요.' : '현재 회원가입이 제한되어 있습니다.' }}</p> <p class="cta-note">{{ !ID_LOGIN_ENABLED || ui.signupEnabled ? '로그인하면 나의 가계부 요약을 볼 수 있어요.' : '현재 회원가입이 제한되어 있습니다.' }}</p>
</div> </div>