Files
core2026/app/gateway-frontend/src/views/AdminOverviewView.vue
T
Hide_D 98fe4f3764 fix(gateway): 운영 개요의 변경 이력 안내 제거
관리자가 현재 수행할 기능과 무관한 작업 영역 재구성 설명을 제거합니다. 현재 행동 안내만 남기고 실제 Chromium에서 문구 부재와 반응형 카드 위치를 검증합니다.
2026-08-21 00:55:35 +00:00

187 lines
5.1 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import AdminConsoleLayout from '../layouts/AdminConsoleLayout.vue';
import { useAuthStore } from '../stores/auth';
import { trpc } from '../utils/trpc';
const auth = useAuthStore();
const capabilities = ref<string[]>([]);
const profileCount = ref(0);
const adminClient = trpc.admin as unknown as {
capabilities: { list: { query: () => Promise<Array<{ permission: string }>> } };
profiles: { list: { query: () => Promise<unknown[]> } };
};
const isRootAdmin = computed(() =>
(auth.user?.roles ?? []).some((role) => role === 'superuser' || role === 'admin' || role === 'admin.superuser')
);
const hasCapability = (permission: string): boolean => isRootAdmin.value || capabilities.value.includes(permission);
const sections = computed(
() =>
[
{
to: '/admin/users',
eyebrow: 'Accounts',
title: '사용자 관리',
description: '계정 조회와 생성, 인증 유예, 권한, 제재, 복구 및 탈퇴 예약을 관리합니다.',
tone: 'blue',
visible: hasCapability('admin.users.manage') || hasCapability('admin.users.create'),
},
{
to: '/admin/servers',
eyebrow: 'Profiles',
title: '서버 관리',
description: '프로필별 공개 정보, 계정 정책, 실행 상태와 게임 운영 동작을 관리합니다.',
tone: 'emerald',
visible: isRootAdmin.value || profileCount.value > 0,
},
{
to: '/admin/releases',
eyebrow: 'Releases',
title: 'Gateway 릴리스',
description: 'Gateway API·frontend·orchestrator 배포와 rollback을 별도 제어면에서 관리합니다.',
tone: 'violet',
visible: hasCapability('admin.releases.manage'),
},
{
to: '/admin/system',
eyebrow: 'System',
title: '공지 · 접속',
description: '로비 공지와 관리자 세션 연결 상태처럼 Gateway 공통 설정을 관리합니다.',
tone: 'amber',
visible: hasCapability('admin.notice.manage'),
},
{
to: '/admin/audit',
eyebrow: 'Audit',
title: '감사 로그',
description: '누가 어떤 관리자 조치를 수행했는지 결과와 대상, 사유를 추적합니다.',
tone: 'zinc',
visible: hasCapability('admin.audit.read'),
},
] as const
);
onMounted(async () => {
try {
capabilities.value = (await adminClient.capabilities.list.query()).map((entry) => entry.permission);
} catch {
capabilities.value = [];
}
try {
profileCount.value = (await adminClient.profiles.list.query()).length;
} catch {
profileCount.value = 0;
}
});
</script>
<template>
<AdminConsoleLayout title="운영 개요" description="관리 대상을 선택하세요." eyebrow="Admin workspace">
<section class="overview-grid" aria-label="관리 기능">
<RouterLink
v-for="section in sections.filter((entry) => entry.visible)"
:key="section.to"
:to="section.to"
class="overview-card"
:class="`tone-${section.tone}`"
>
<span>{{ section.eyebrow }}</span>
<h2>{{ section.title }}</h2>
<p>{{ section.description }}</p>
<strong>열기 </strong>
</RouterLink>
</section>
</AdminConsoleLayout>
</template>
<style scoped>
.overview-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.overview-card {
min-height: 190px;
border: 1px solid #27272a;
border-radius: 10px;
background: #111113;
padding: 22px;
color: #fafafa;
text-decoration: none;
transition:
border-color 140ms ease,
transform 140ms ease,
background-color 140ms ease;
}
.overview-card:hover,
.overview-card:focus-visible {
border-color: var(--accent);
background: #18181b;
outline: none;
transform: translateY(-2px);
}
.overview-card > span {
color: var(--accent);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.overview-card h2 {
margin: 15px 0 9px;
font-size: 19px;
}
.overview-card p {
min-height: 58px;
margin: 0 0 20px;
color: #a1a1aa;
font-size: 12px;
line-height: 1.65;
}
.overview-card strong {
color: #d4d4d8;
font-size: 12px;
}
.tone-blue {
--accent: #60a5fa;
}
.tone-emerald {
--accent: #34d399;
}
.tone-violet {
--accent: #a78bfa;
}
.tone-amber {
--accent: #fbbf24;
}
.tone-zinc {
--accent: #a1a1aa;
}
@media (max-width: 700px) {
.overview-grid {
grid-template-columns: 1fr;
}
.overview-card {
min-height: 0;
}
.overview-card p {
min-height: 0;
}
}
</style>