feat(gateway): reorganize admin console navigation

This commit is contained in:
2026-08-08 13:36:37 +00:00
parent d99e18f334
commit 4d46afa6a5
16 changed files with 804 additions and 922 deletions
@@ -0,0 +1,345 @@
<script setup lang="ts">
import { ref } from 'vue';
import DefaultLayout from './DefaultLayout.vue';
defineProps<{
title: string;
description: string;
eyebrow?: string;
}>();
const menuOpen = ref(false);
const navigation = [
{
label: '관리',
items: [
{ to: '/admin', label: '운영 개요', icon: '⌂', exact: true },
{ to: '/admin/users', label: '사용자 관리', icon: '人', exact: false },
],
},
{
label: '서비스 운영',
items: [
{ to: '/admin/servers', label: '서버 관리', icon: '◫', exact: false },
{ to: '/admin/releases', label: '버전 업데이트', icon: '↥', exact: false },
],
},
{
label: '시스템',
items: [
{ to: '/admin/system', label: '공지 · 접속', icon: '⚙', exact: false },
{ to: '/admin/audit', label: '감사 로그', icon: '≡', exact: false },
],
},
] as const;
</script>
<template>
<DefaultLayout>
<div class="admin-shell">
<button
class="admin-menu-button"
type="button"
:aria-expanded="menuOpen"
aria-controls="admin-navigation"
@click="menuOpen = !menuOpen"
>
<span>관리자 메뉴</span>
<span aria-hidden="true">{{ menuOpen ? '닫기' : '열기' }}</span>
</button>
<aside id="admin-navigation" class="admin-sidebar" :class="{ open: menuOpen }">
<div class="admin-identity">
<span class="admin-mark" aria-hidden="true"></span>
<div>
<strong>관리자 콘솔</strong>
<span>Gateway control</span>
</div>
</div>
<nav aria-label="관리자 메뉴">
<section v-for="group in navigation" :key="group.label" class="admin-nav-group">
<h2>{{ group.label }}</h2>
<RouterLink
v-for="item in group.items"
:key="item.to"
:to="item.to"
class="admin-nav-link"
:active-class="item.exact ? '' : 'active'"
:exact-active-class="item.exact ? 'active' : ''"
@click="menuOpen = false"
>
<span class="admin-nav-icon" aria-hidden="true">{{ item.icon }}</span>
<span>{{ item.label }}</span>
</RouterLink>
</section>
</nav>
<RouterLink class="admin-exit" to="/lobby"> 로비로 돌아가기</RouterLink>
</aside>
<main class="admin-content">
<header class="admin-page-header">
<div>
<p>{{ eyebrow ?? 'Admin console' }}</p>
<h1>{{ title }}</h1>
<div class="admin-description">{{ description }}</div>
</div>
<div v-if="$slots.actions" class="admin-header-actions">
<slot name="actions" />
</div>
</header>
<slot />
</main>
</div>
</DefaultLayout>
</template>
<style scoped>
.admin-shell {
display: grid;
width: min(1480px, 100%);
min-height: calc(100vh - 56px);
margin: 0 auto;
grid-template-columns: 244px minmax(0, 1fr);
padding-top: 56px;
background: #09090b;
}
:deep(.gateway-navbar) {
position: absolute;
}
.admin-sidebar {
position: sticky;
top: 56px;
height: calc(100vh - 56px);
border-right: 1px solid #27272a;
background: #111113;
padding: 24px 16px;
}
.admin-identity {
display: flex;
align-items: center;
gap: 11px;
margin-bottom: 28px;
padding: 0 8px;
}
.admin-identity strong,
.admin-identity span {
display: block;
}
.admin-identity strong {
color: #fafafa;
font-size: 15px;
}
.admin-identity div > span {
margin-top: 2px;
color: #71717a;
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.admin-mark {
display: grid;
width: 34px;
height: 34px;
flex: 0 0 34px;
place-items: center;
border: 1px solid #a16207;
border-radius: 8px;
background: #422006;
color: #facc15;
font-weight: 800;
}
.admin-nav-group {
margin: 0 0 24px;
}
.admin-nav-group h2 {
margin: 0 8px 7px;
color: #71717a;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.admin-nav-link {
display: flex;
min-height: 40px;
align-items: center;
gap: 10px;
margin: 2px 0;
border: 1px solid transparent;
border-radius: 7px;
padding: 8px 10px;
color: #a1a1aa;
font-size: 13px;
text-decoration: none;
transition:
border-color 120ms ease,
background-color 120ms ease,
color 120ms ease;
}
.admin-nav-link:hover,
.admin-nav-link:focus-visible {
border-color: #3f3f46;
background: #1c1c1f;
color: #fafafa;
outline: none;
}
.admin-nav-link.active {
border-color: #713f12;
background: #2d1b08;
color: #fde68a;
}
.admin-nav-icon {
width: 20px;
color: #d4d4d8;
font-family: sans-serif;
font-size: 15px;
text-align: center;
}
.admin-exit {
position: absolute;
right: 24px;
bottom: 24px;
left: 24px;
border-top: 1px solid #27272a;
padding-top: 17px;
color: #71717a;
font-size: 12px;
text-decoration: none;
}
.admin-exit:hover,
.admin-exit:focus-visible {
color: #e4e4e7;
}
.admin-content {
min-width: 0;
padding: 30px clamp(20px, 3vw, 44px) 56px;
}
.admin-page-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
margin-bottom: 28px;
border-bottom: 1px solid #27272a;
padding-bottom: 22px;
}
.admin-page-header p {
margin: 0 0 7px;
color: #ca8a04;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.admin-page-header h1 {
margin: 0;
color: #fafafa;
font-size: clamp(25px, 3vw, 34px);
line-height: 1.2;
}
.admin-description {
margin-top: 8px;
color: #a1a1aa;
font-size: 13px;
line-height: 1.6;
}
.admin-header-actions {
flex: 0 0 auto;
}
.admin-menu-button {
display: none;
}
@media (max-width: 860px) {
.admin-shell {
display: block;
padding-top: 72px;
}
.admin-menu-button {
position: absolute;
z-index: 20;
top: 72px;
right: 16px;
left: 16px;
display: flex;
min-height: 44px;
align-items: center;
justify-content: space-between;
border: 1px solid #3f3f46;
border-radius: 8px;
background: #18181b;
padding: 10px 14px;
color: #f4f4f5;
font-size: 13px;
}
.admin-sidebar {
position: absolute;
z-index: 19;
top: 124px;
right: 16px;
left: 16px;
display: none;
width: auto;
height: auto;
border: 1px solid #3f3f46;
border-radius: 10px;
box-shadow: 0 20px 45px rgb(0 0 0 / 55%);
padding: 18px 14px 14px;
}
.admin-sidebar.open {
display: block;
}
.admin-identity {
margin-bottom: 18px;
}
.admin-nav-group {
margin-bottom: 14px;
}
.admin-exit {
position: static;
display: block;
margin: 16px 8px 0;
}
.admin-content {
padding: 76px 16px 40px;
}
.admin-page-header {
align-items: flex-start;
flex-direction: column;
gap: 14px;
}
}
</style>