feat: 가계부·게시판 프론트엔드 구현 + Slim Budget 브랜딩/모바일 대응
- 가계부: 대시보드(예산 대비 지출·분류별 파이·기간별 막대·순자산 추이), 내역(검색·필터·태그), 계좌(은행/카드/대출/투자), 예산, 분류, 정기 거래, 태그, 투자 포트폴리오(종목·매수/매도·평가손익) - 게시판: 목록/상세/작성(마크다운)/댓글/태그/열람제한 - 공통: 인증(Bearer 토큰·401 처리), 모바일 반응형(드로어·아이콘 버튼), Slim Budget 브랜딩(로고/타이틀/푸터), 홈 대시보드 자리 - 문서: docs/FRONTEND.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+88
-60
@@ -1,86 +1,114 @@
|
||||
<script setup>
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import { onMounted, onUnmounted, watch } from 'vue'
|
||||
import { RouterView, useRoute } from 'vue-router'
|
||||
import AppHeader from '@/components/layout/AppHeader.vue'
|
||||
import AppSidebar from '@/components/layout/AppSidebar.vue'
|
||||
import AppFooter from '@/components/layout/AppFooter.vue'
|
||||
import LoginModal from '@/components/LoginModal.vue'
|
||||
import SignupModal from '@/components/SignupModal.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useUiStore } from '@/stores/ui'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const ui = useUiStore()
|
||||
const route = useRoute()
|
||||
|
||||
// http.js 의 401 처리에서 발생시키는 이벤트 → 세션 정리 후 로그인 팝업
|
||||
function onUnauthorized() {
|
||||
auth.clear()
|
||||
ui.openLogin()
|
||||
}
|
||||
onMounted(() => window.addEventListener('auth:unauthorized', onUnauthorized))
|
||||
onUnmounted(() => window.removeEventListener('auth:unauthorized', onUnauthorized))
|
||||
|
||||
// 페이지 이동 시 모바일 사이드바 자동 닫힘
|
||||
watch(() => route.fullPath, () => ui.closeSidebar())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
|
||||
<div class="layout" :class="{ 'sidebar-open': ui.sidebarOpen }">
|
||||
<AppHeader class="layout-top" />
|
||||
<AppSidebar class="layout-left" />
|
||||
<div class="sidebar-backdrop" @click="ui.closeSidebar()"></div>
|
||||
<main class="layout-body">
|
||||
<RouterView />
|
||||
</main>
|
||||
<AppFooter class="layout-bottom" />
|
||||
</div>
|
||||
|
||||
<div class="wrapper">
|
||||
<HelloWorld msg="You did it!" />
|
||||
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
<RouterLink to="/users">Users</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<RouterView />
|
||||
<LoginModal />
|
||||
<SignupModal />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
line-height: 1.5;
|
||||
max-height: 100vh;
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
'top top'
|
||||
'left body'
|
||||
'bottom bottom';
|
||||
grid-template-columns: 220px 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 2rem;
|
||||
.layout-top {
|
||||
grid-area: top;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
margin-top: 2rem;
|
||||
.layout-left {
|
||||
grid-area: left;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active {
|
||||
color: var(--color-text);
|
||||
.layout-body {
|
||||
grid-area: body;
|
||||
padding: 1.5rem 2rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active:hover {
|
||||
background-color: transparent;
|
||||
.layout-bottom {
|
||||
grid-area: bottom;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid var(--color-border);
|
||||
.sidebar-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
/* 모바일: 사이드바를 오프캔버스 드로어로 */
|
||||
@media (max-width: 768px) {
|
||||
.layout {
|
||||
grid-template-areas:
|
||||
'top'
|
||||
'body'
|
||||
'bottom';
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
.layout-left {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 1200;
|
||||
width: 78%;
|
||||
max-width: 280px;
|
||||
overflow-y: auto;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.22s ease;
|
||||
box-shadow: 2px 0 16px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
.layout.sidebar-open .layout-left {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: left;
|
||||
margin-left: -1rem;
|
||||
font-size: 1rem;
|
||||
|
||||
padding: 1rem 0;
|
||||
margin-top: 1rem;
|
||||
.layout.sidebar-open .sidebar-backdrop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1150;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
.layout-body {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user