2026-05-30 21:18:13 +09:00
|
|
|
<script setup>
|
2026-05-31 15:42:52 +09:00
|
|
|
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'
|
2026-05-31 18:46:57 +09:00
|
|
|
import ChangePasswordModal from '@/components/ChangePasswordModal.vue'
|
2026-05-31 15:42:52 +09:00
|
|
|
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())
|
2026-05-30 21:18:13 +09:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-05-31 15:42:52 +09:00
|
|
|
<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 />
|
2026-05-31 18:46:57 +09:00
|
|
|
<ChangePasswordModal />
|
2026-05-30 21:18:13 +09:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-05-31 15:42:52 +09:00
|
|
|
.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;
|
2026-05-30 21:18:13 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 15:42:52 +09:00
|
|
|
.layout-top {
|
|
|
|
|
grid-area: top;
|
2026-05-30 21:18:13 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 15:42:52 +09:00
|
|
|
.layout-left {
|
|
|
|
|
grid-area: left;
|
2026-05-30 21:18:13 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 15:42:52 +09:00
|
|
|
.layout-body {
|
|
|
|
|
grid-area: body;
|
|
|
|
|
padding: 1.5rem 2rem;
|
|
|
|
|
overflow: auto;
|
2026-05-30 21:18:13 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 15:42:52 +09:00
|
|
|
.layout-bottom {
|
|
|
|
|
grid-area: bottom;
|
2026-05-30 21:18:13 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 15:42:52 +09:00
|
|
|
.sidebar-backdrop {
|
|
|
|
|
display: none;
|
2026-05-30 21:18:13 +09:00
|
|
|
}
|
|
|
|
|
|
2026-05-31 15:42:52 +09:00
|
|
|
/* 모바일: 사이드바를 오프캔버스 드로어로 */
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
.layout {
|
|
|
|
|
grid-template-areas:
|
|
|
|
|
'top'
|
|
|
|
|
'body'
|
|
|
|
|
'bottom';
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
grid-template-rows: auto 1fr auto;
|
2026-05-30 21:18:13 +09:00
|
|
|
}
|
2026-05-31 15:42:52 +09:00
|
|
|
.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);
|
2026-05-30 21:18:13 +09:00
|
|
|
}
|
2026-05-31 15:42:52 +09:00
|
|
|
.layout.sidebar-open .layout-left {
|
|
|
|
|
transform: translateX(0);
|
2026-05-30 21:18:13 +09:00
|
|
|
}
|
2026-05-31 15:42:52 +09:00
|
|
|
.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;
|
2026-05-30 21:18:13 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|