Files
sb-front/src/App.vue
T
ByungCheol 13308a82ef feat: 가계부·게시판 프론트엔드 구현 + Slim Budget 브랜딩/모바일 대응
- 가계부: 대시보드(예산 대비 지출·분류별 파이·기간별 막대·순자산 추이),
  내역(검색·필터·태그), 계좌(은행/카드/대출/투자), 예산, 분류, 정기 거래,
  태그, 투자 포트폴리오(종목·매수/매도·평가손익)
- 게시판: 목록/상세/작성(마크다운)/댓글/태그/열람제한
- 공통: 인증(Bearer 토큰·401 처리), 모바일 반응형(드로어·아이콘 버튼),
  Slim Budget 브랜딩(로고/타이틀/푸터), 홈 대시보드 자리
- 문서: docs/FRONTEND.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:42:52 +09:00

115 lines
2.6 KiB
Vue

<script setup>
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>
<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>
<LoginModal />
<SignupModal />
</template>
<style scoped>
.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;
}
.layout-top {
grid-area: top;
}
.layout-left {
grid-area: left;
}
.layout-body {
grid-area: body;
padding: 1.5rem 2rem;
overflow: auto;
}
.layout-bottom {
grid-area: bottom;
}
.sidebar-backdrop {
display: none;
}
/* 모바일: 사이드바를 오프캔버스 드로어로 */
@media (max-width: 768px) {
.layout {
grid-template-areas:
'top'
'body'
'bottom';
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
}
.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);
}
.layout.sidebar-open .layout-left {
transform: translateX(0);
}
.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>