2025-08-08 13:11:33 +09:00
|
|
|
|
<script setup lang="ts">
|
2025-08-22 14:01:30 +09:00
|
|
|
|
import AppHeader from "../components/layout/AppHeader.vue";
|
2025-09-23 14:15:32 +09:00
|
|
|
|
import { ref, computed, watch, onMounted } from "vue";
|
2025-08-22 14:01:30 +09:00
|
|
|
|
import { useRouter } from "vue-router";
|
2025-08-14 11:00:48 +09:00
|
|
|
|
import { useTabsStore } from "../stores/tab";
|
2025-09-23 14:15:32 +09:00
|
|
|
|
import { usePermissionsStore } from "~/stores/permissions";
|
2025-08-08 13:11:33 +09:00
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
2025-09-23 14:15:32 +09:00
|
|
|
|
const activeMenu = ref("HOME");
|
2025-08-08 13:11:33 +09:00
|
|
|
|
const showSubmenuBar = ref(false);
|
|
|
|
|
|
|
2025-08-14 11:00:48 +09:00
|
|
|
|
const tabsStore = useTabsStore();
|
2025-09-23 14:15:32 +09:00
|
|
|
|
const permissionStore = usePermissionsStore();
|
|
|
|
|
|
|
|
|
|
|
|
// 권한 초기화
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await permissionStore.fetchPermissions();
|
|
|
|
|
|
});
|
2025-08-14 11:00:48 +09:00
|
|
|
|
|
2025-08-22 14:01:30 +09:00
|
|
|
|
// 메뉴 클릭 시 홈 이동
|
2025-09-22 11:20:12 +09:00
|
|
|
|
watch(activeMenu, newValue => {
|
2025-09-23 14:15:32 +09:00
|
|
|
|
if (newValue === "HOME") router.push("/");
|
2025-08-08 13:11:33 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-23 14:15:32 +09:00
|
|
|
|
// 권한 기반 서브메뉴 생성
|
2025-08-08 13:11:33 +09:00
|
|
|
|
const subMenus = computed(() => {
|
2025-09-23 14:15:32 +09:00
|
|
|
|
if (activeMenu.value === "HOME") return [];
|
|
|
|
|
|
|
2025-09-23 15:21:00 +09:00
|
|
|
|
// 활성 메뉴의 코드 찾기 (PG01, PG02 등)
|
2025-09-23 14:15:32 +09:00
|
|
|
|
const activeMenuCode = activeMenu.value;
|
|
|
|
|
|
|
2025-09-23 15:21:00 +09:00
|
|
|
|
// 해당 페이지그룹의 하위 페이지들 필터링 (menu_yn이 "Y"인 것만)
|
2025-09-23 14:15:32 +09:00
|
|
|
|
return permissionStore.permissions.resources.pages
|
|
|
|
|
|
.filter(page => page.parentCode === activeMenuCode)
|
2025-09-23 15:21:00 +09:00
|
|
|
|
.filter(page => page.menuYn === "Y") // 메뉴에 표시할 페이지만
|
2025-09-23 14:15:32 +09:00
|
|
|
|
.filter(page => permissionStore.hasPagePermission(page.path || ""))
|
|
|
|
|
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
|
|
|
|
|
.map(page => ({
|
|
|
|
|
|
key: page.code,
|
|
|
|
|
|
label: page.name,
|
|
|
|
|
|
to: page.path || "",
|
|
|
|
|
|
componentName: page.name,
|
|
|
|
|
|
}));
|
2025-08-08 13:11:33 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function onMenuClick(menu: string) {
|
2025-09-23 14:15:32 +09:00
|
|
|
|
showSubmenuBar.value = menu !== "home";
|
2025-08-08 13:11:33 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-22 14:01:30 +09:00
|
|
|
|
// ✅ 서브메뉴 클릭 → 현재 활성 탭 내용만 변경
|
2025-09-22 11:20:12 +09:00
|
|
|
|
function onSubMenuClick(sub: {
|
|
|
|
|
|
key: string;
|
|
|
|
|
|
label: string;
|
|
|
|
|
|
to: string;
|
|
|
|
|
|
componentName: string;
|
|
|
|
|
|
}) {
|
2025-08-22 14:01:30 +09:00
|
|
|
|
tabsStore.updateActiveTab(sub);
|
|
|
|
|
|
// const activeKey = tabsStore.activeTab;
|
|
|
|
|
|
// router.push(`/${activeKey}${sub.to}`);
|
2025-08-14 11:00:48 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-22 14:01:30 +09:00
|
|
|
|
// ✅ 새 탭 추가 버튼
|
|
|
|
|
|
function addNewTab() {
|
|
|
|
|
|
tabsStore.addTab();
|
|
|
|
|
|
// router.push(`/${key}/`);
|
2025-08-08 13:11:33 +09:00
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
2025-08-14 11:00:48 +09:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="layout">
|
|
|
|
|
|
<AppHeader v-model="activeMenu" @update:model-value="onMenuClick" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 서브메뉴 바 -->
|
2025-09-22 11:20:12 +09:00
|
|
|
|
<nav
|
2025-09-23 14:15:32 +09:00
|
|
|
|
v-if="showSubmenuBar && subMenus.length > 0"
|
|
|
|
|
|
class="w-full bg-gray-100 shadow-sm px-4 py-2"
|
2025-09-22 11:20:12 +09:00
|
|
|
|
>
|
2025-09-23 14:15:32 +09:00
|
|
|
|
<div class="flex items-center space-x-6">
|
|
|
|
|
|
<span class="text-sm font-medium text-gray-600 mr-4">
|
|
|
|
|
|
{{ activeMenu }}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div class="flex space-x-4">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="sub in subMenus"
|
|
|
|
|
|
:key="sub.key"
|
|
|
|
|
|
class="submenu-btn"
|
|
|
|
|
|
@click="onSubMenuClick(sub)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ sub.label }}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-14 11:00:48 +09:00
|
|
|
|
</nav>
|
2025-09-22 11:20:12 +09:00
|
|
|
|
<br /><br />
|
2025-08-22 14:01:30 +09:00
|
|
|
|
<!-- 탭 바 -->
|
|
|
|
|
|
<div class="tab-bar">
|
2025-08-14 11:00:48 +09:00
|
|
|
|
<div
|
|
|
|
|
|
v-for="tab in tabsStore.tabs"
|
|
|
|
|
|
:key="tab.key"
|
|
|
|
|
|
class="tab-item"
|
|
|
|
|
|
:class="{ active: tabsStore.activeTab === tab.key }"
|
2025-09-22 11:20:12 +09:00
|
|
|
|
@click="tabsStore.setActiveTab(tab.key)"
|
2025-08-14 11:00:48 +09:00
|
|
|
|
>
|
|
|
|
|
|
{{ tab.label }}
|
2025-09-22 11:20:12 +09:00
|
|
|
|
<span
|
|
|
|
|
|
v-show="tabsStore.activeTab !== tab.key"
|
|
|
|
|
|
class="close-btn"
|
|
|
|
|
|
@click.stop="tabsStore.removeTab(tab.key)"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</span>
|
2025-08-14 11:00:48 +09:00
|
|
|
|
</div>
|
2025-08-22 14:01:30 +09:00
|
|
|
|
|
|
|
|
|
|
<!-- ✅ 새 탭 추가 버튼 -->
|
|
|
|
|
|
<button class="add-tab-btn" @click="addNewTab">+</button>
|
2025-08-14 11:00:48 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<main class="main">
|
|
|
|
|
|
<slot />
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2025-08-08 13:11:33 +09:00
|
|
|
|
<style scoped>
|
|
|
|
|
|
.layout {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
.main {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
padding: 2rem;
|
|
|
|
|
|
padding-top: 0.5rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.footer {
|
|
|
|
|
|
background: #f8f9fa;
|
|
|
|
|
|
padding: 1rem;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
border-top: 1px solid #e9ecef;
|
|
|
|
|
|
}
|
|
|
|
|
|
.submenu-bar {
|
|
|
|
|
|
background: #f4f6fa;
|
|
|
|
|
|
border-bottom: 1px solid #e0e7ef;
|
|
|
|
|
|
padding: 0.5rem 2rem;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 80px;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
.submenu-btn {
|
2025-09-23 14:15:32 +09:00
|
|
|
|
padding: 0.25rem 0.75rem;
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
color: #374151;
|
2025-08-08 13:11:33 +09:00
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
2025-09-23 14:15:32 +09:00
|
|
|
|
border-radius: 0.25rem;
|
2025-08-08 13:11:33 +09:00
|
|
|
|
cursor: pointer;
|
2025-09-23 14:15:32 +09:00
|
|
|
|
transition: all 0.15s ease;
|
2025-08-08 13:11:33 +09:00
|
|
|
|
}
|
2025-09-23 14:15:32 +09:00
|
|
|
|
|
2025-08-08 13:11:33 +09:00
|
|
|
|
.submenu-btn:hover {
|
2025-09-23 14:15:32 +09:00
|
|
|
|
color: #2563eb;
|
|
|
|
|
|
background-color: #eff6ff;
|
2025-08-08 13:11:33 +09:00
|
|
|
|
}
|
2025-08-14 11:00:48 +09:00
|
|
|
|
|
|
|
|
|
|
/* 탭바 스타일 */
|
|
|
|
|
|
.tab-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 0.4rem 0.8rem;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-bottom: 1px solid #ddd;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tab-item {
|
|
|
|
|
|
padding: 0.3rem 0.8rem;
|
|
|
|
|
|
background: #f2f2f2;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tab-item.active {
|
|
|
|
|
|
background: #1976d2;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
.close-btn {
|
|
|
|
|
|
margin-left: 6px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
2025-08-08 13:11:33 +09:00
|
|
|
|
</style>
|