Files
bowlingManager/frontend/src/views/admin/Dashboard.vue
T
2025-07-03 08:36:26 +09:00

341 lines
8.6 KiB
Vue

<template>
<div class="admin-dashboard">
<h2>관리자 대시보드</h2>
<div v-if="loading" class="loading-spinner">
<ProgressSpinner />
</div>
<div v-else class="grid">
<!-- 요약 카드 -->
<div class="col-12 md:col-6 lg:col-3">
<div class="dashboard-card">
<div class="card-header">
<i class="pi pi-users"></i>
<h3>사용자</h3>
</div>
<div class="card-content">
<p> 사용자 : {{ summary.totalUsers }}</p>
</div>
<div class="card-footer">
<Button label="사용자 관리" icon="pi pi-arrow-right" @click="navigateTo('/admin/users')" />
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="dashboard-card">
<div class="card-header">
<i class="pi pi-building"></i>
<h3>클럽</h3>
</div>
<div class="card-content">
<p> 클럽 : {{ summary.totalClubs }}</p>
</div>
<div class="card-footer">
<Button label="클럽 관리" icon="pi pi-arrow-right" @click="navigateTo('/admin/clubs')" />
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="dashboard-card">
<div class="card-header">
<i class="pi pi-credit-card"></i>
<h3>구독</h3>
</div>
<div class="card-content">
<p>활성 구독: {{ summary.activeSubscriptions }}</p>
</div>
<div class="card-footer">
<Button label="구독 관리" icon="pi pi-arrow-right" @click="navigateTo('/admin/subscriptions')" />
</div>
</div>
</div>
<div class="col-12 md:col-6 lg:col-3">
<div class="dashboard-card">
<div class="card-header">
<i class="pi pi-history"></i>
<h3>활동 로그</h3>
</div>
<div class="card-content">
<p>시스템 활동 기록</p>
</div>
<div class="card-footer">
<Button label="활동 로그" icon="pi pi-arrow-right" @click="navigateTo('/admin/activity-logs')" />
</div>
</div>
</div>
<!-- 최근 데이터 -->
<div class="col-12 md:col-6">
<div class="dashboard-card">
<div class="card-header">
<i class="pi pi-users"></i>
<h3>최근 가입한 사용자</h3>
</div>
<div class="card-content">
<DataTable :value="recent.users" :rows="5" stripedRows>
<Column field="name" header="이름"></Column>
<Column field="email" header="이메일"></Column>
<Column field="role" header="권한"></Column>
<Column field="createdAt" header="가입일">
<template #body="{ data }">
{{ formatDate(data.createdAt) }}
</template>
</Column>
</DataTable>
</div>
</div>
</div>
<div class="col-12 md:col-6">
<div class="dashboard-card">
<div class="card-header">
<i class="pi pi-building"></i>
<h3>최근 생성된 클럽</h3>
</div>
<div class="card-content">
<DataTable :value="recent.clubs" :rows="5" stripedRows>
<Column field="name" header="클럽명"></Column>
<Column field="memberCount" header="회원 수"></Column>
<Column field="owner.name" header="모임장"></Column>
<Column field="createdAt" header="생성일">
<template #body="{ data }">
{{ formatDate(data.createdAt) }}
</template>
</Column>
</DataTable>
</div>
</div>
</div>
<!-- 월별 통계 차트 -->
<div class="col-12">
<div class="dashboard-card">
<div class="card-header">
<i class="pi pi-chart-line"></i>
<h3>월별 통계</h3>
</div>
<div class="card-content">
<Chart type="line" :data="chartData" :options="chartOptions" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { useGtag } from 'vue-gtag-next';
import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import dayjs from 'dayjs';
import adminService from '@/services/adminService';
// 컴포넌트들은 main.js에서 전역으로 등록되어 있으므로 import 불필요
const router = useRouter();
const toast = useToast();
// 데이터 상태
const loading = ref(true);
const summary = ref({
totalUsers: 0,
totalClubs: 0,
activeSubscriptions: 0
});
const recent = ref({
users: [],
clubs: [],
subscriptions: []
});
const stats = ref({
users: [],
clubs: [],
subscriptions: []
});
// 차트 데이터 계산
const chartData = computed(() => {
const months = stats.value.users.map(item => item.month);
return {
labels: months,
datasets: [
{
label: '사용자',
data: stats.value.users.map(item => item.count),
borderColor: '#42A5F5',
tension: 0.4
},
{
label: '클럽',
data: stats.value.clubs.map(item => item.count),
borderColor: '#66BB6A',
tension: 0.4
},
{
label: '구독',
data: stats.value.subscriptions.map(item => item.count),
borderColor: '#FFA726',
tension: 0.4
}
]
};
});
// 차트 옵션
const chartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
};
// 컴포넌트 마운트 시 데이터 로드
// 페이지 진입 시 Google Analytics 페이지뷰 추적
onMounted(async () => {
const gtag = useGtag();
gtag.pageview({
page_title: '관리자대시보드',
page_path: window.location.pathname,
page_location: window.location.href
});
try {
loading.value = true;
await loadDashboardData();
} catch (error) {
console.error('관리자 대시보드 데이터 로딩 오류:', error);
} finally {
loading.value = false;
}
});
// 대시보드 데이터 로드 함수
const loadDashboardData = async () => {
try {
const response = await adminService.getDashboardData();
const data = response.data;
summary.value = data.summary;
recent.value = data.recent;
stats.value = data.stats;
} catch (error) {
console.error('관리자 대시보드 데이터 로딩 중 오류 발생:', error);
toast.add({ severity: 'error', summary: '오류', detail: '관리자 대시보드 데이터를 가져오는 중 오류가 발생했습니다.', life: 3000 });
}
};
// 날짜 포맷 함수
const formatDate = (date) => {
return dayjs(date).format('YYYY-MM-DD');
};
const navigateTo = (path) => {
router.push(path);
};
</script>
<style scoped>
.admin-dashboard {
padding: 1rem;
}
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.dashboard-card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
height: 100%;
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.card-header i {
font-size: 1.5rem;
margin-right: 0.5rem;
color: #3B82F6;
}
.card-header h3 {
margin: 0;
font-size: 1.25rem;
}
.card-content {
flex: 1;
margin-bottom: 1rem;
}
.card-footer {
display: flex;
justify-content: flex-end;
}
:deep(.p-datatable) {
box-shadow: none;
}
:deep(.p-datatable .p-datatable-thead > tr > th) {
background-color: #f8f9fa;
}
:deep(.p-datatable .p-datatable-tbody > tr) {
background-color: transparent;
}
@media (max-width: 600px) {
.admin-dashboard, .header, .dashboard-card, .activity-log-container, .subscription-management-container, .user-management-container, .club-management {
padding: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
.dashboard-card {
padding: 0.7rem !important;
font-size: 0.95rem;
}
.header h2, .dashboard-card h3 {
font-size: 1.1rem !important;
}
.p-datatable-wrapper {
overflow-x: auto !important;
}
/* Dialog 팝업 중앙 정렬 개선 */
:deep(.p-dialog) {
align-items: flex-start !important;
justify-content: center !important;
padding-top: 2.5rem !important;
}
:deep(.p-dialog .p-dialog-content) {
margin-top: 0 !important;
}
}
</style>