335 lines
9.7 KiB
Vue
335 lines
9.7 KiB
Vue
<template>
|
|
<div class="club-dashboard">
|
|
<h2>클럽 대시보드</h2>
|
|
|
|
<div v-if="loading" class="card flex justify-content-center">
|
|
<ProgressSpinner />
|
|
</div>
|
|
|
|
<div v-else class="grid">
|
|
<!-- 회원 통계 카드 -->
|
|
<div class="col-12 md:col-6 lg:col-6">
|
|
<div class="dashboard-card">
|
|
<div class="card-header">
|
|
<i class="pi pi-users"></i>
|
|
<h3>회원 현황</h3>
|
|
</div>
|
|
<div class="card-content">
|
|
<p>총 회원: {{ memberStats.totalCount }}명</p>
|
|
<p>정회원: {{ memberStats.regularCount }}명</p>
|
|
<p>준회원: {{ memberStats.associateCount }}명</p>
|
|
</div>
|
|
<div class="card-footer">
|
|
<Button label="회원 관리" icon="pi pi-arrow-right" @click="navigateTo('/club/members')" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 점수 통계 카드 -->
|
|
<div class="col-12 md:col-6 lg:col-6">
|
|
<div class="dashboard-card">
|
|
<div class="card-header">
|
|
<i class="pi pi-chart-line"></i>
|
|
<h3>점수 통계</h3>
|
|
</div>
|
|
<div class="card-content">
|
|
<p>최고 게임: {{ scoreStats.topGame || 0 }}점</p>
|
|
<p>평균 점수: {{ scoreStats.averageScore || 0 }}점</p>
|
|
<p>전체 게임: {{ scoreStats.totalGames || 0 }}게임</p>
|
|
</div>
|
|
<div class="card-footer">
|
|
<Button label="통계 보기" icon="pi pi-arrow-right" @click="navigateTo('/club/statistics')" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- 다음 모임 카드(여러 개) -->
|
|
<div class="col-12 md:col-6 lg:col-6">
|
|
<div class="dashboard-card">
|
|
<div class="card-header">
|
|
<i class="pi pi-calendar"></i>
|
|
<h3>다음 모임</h3>
|
|
</div>
|
|
<div class="card-content" v-if="nextMeetings && nextMeetings.length">
|
|
<table class="recent-meetings-table" style="width:100%; border-collapse:collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th>일시</th>
|
|
<th>제목</th>
|
|
<th>장소</th>
|
|
<th>참가자</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="meeting in nextMeetings.slice(0, 3)" :key="meeting.id || meeting.date">
|
|
<td>{{ formatDate(meeting.date) }}</td>
|
|
<td>{{ meeting.title || '-' }}</td>
|
|
<td>{{ meeting.location || '미정' }}</td>
|
|
<td>{{ meeting.participantsCount || meeting.participants || 0 }}명</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card-content" v-else>
|
|
<p>예정된 모임이 없습니다.</p>
|
|
</div>
|
|
<div class="card-footer">
|
|
<Button label="모임 관리" icon="pi pi-arrow-right" @click="navigateTo('/club/events')" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 최근 모임 카드: 최근 3개 리스트로 -->
|
|
<div class="col-12 md:col-6 lg:col-6">
|
|
<div class="dashboard-card">
|
|
<div class="card-header">
|
|
<i class="pi pi-chart-bar"></i>
|
|
<h3>최근 모임</h3>
|
|
</div>
|
|
<div class="card-content">
|
|
<table class="recent-meetings-table" style="width:100%; border-collapse:collapse;">
|
|
<thead>
|
|
<tr>
|
|
<th>일시</th>
|
|
<th>제목</th>
|
|
<th>참가자</th>
|
|
<th>게임 수</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="meeting in recentMeetings.slice(0, 3)" :key="meeting.id || meeting.date">
|
|
<td>{{ formatDate(meeting.date) }}</td>
|
|
<td>{{ meeting.title }}</td>
|
|
<td>{{ meeting.participantsCount || meeting.participants || 0 }}명</td>
|
|
<td>{{ meeting.games }}게임</td>
|
|
</tr>
|
|
<tr v-if="recentMeetings.length === 0">
|
|
<td colspan="4" style="text-align:center;">최근 모임 데이터가 없습니다.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="card-footer">
|
|
<Button label="기록 보기" icon="pi pi-arrow-right" @click="navigateTo('/club/events')" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="grid mt-4">
|
|
<!-- 상위 회원 순위 -->
|
|
<div class="col-12">
|
|
<div class="dashboard-card">
|
|
<div class="card-header">
|
|
<h3>에버리지 순위</h3>
|
|
</div>
|
|
<DataTable :value="topMembers" :rows="5" stripedRows responsiveLayout="scroll">
|
|
<template #empty>
|
|
<tr>
|
|
<td colspan="5" style="text-align:center;">순위 데이터가 없습니다.</td>
|
|
</tr>
|
|
</template>
|
|
<Column header="순위" :body="(_, { rowIndex }) => rowIndex + 1" style="width: 10%"></Column>
|
|
<Column field="name" header="이름" style="width: 30%"></Column>
|
|
<Column field="average" header="에버리지" :body="averageTemplate" style="width: 20%"></Column>
|
|
<Column field="games" header="게임 수" style="width: 20%"></Column>
|
|
<Column field="handicap" header="핸디캡" style="width: 20%"></Column>
|
|
</DataTable>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import apiClient from '@/services/api';
|
|
import dayjs from 'dayjs';
|
|
import { useToast } from 'primevue/usetoast';
|
|
import { useGtag } from 'vue-gtag-next';
|
|
|
|
const router = useRouter();
|
|
const toast = useToast();
|
|
const gtag = useGtag();
|
|
|
|
// 데이터 상태 변수들
|
|
const loading = ref(false);
|
|
const clubId = ref(null);
|
|
|
|
// 회원 통계
|
|
const memberStats = ref({
|
|
totalCount: 0,
|
|
regularCount: 0,
|
|
associateCount: 0
|
|
});
|
|
|
|
// 다음 모임 정보 (여러 개)
|
|
const nextMeetings = ref([]);
|
|
|
|
// 최근 모임 정보
|
|
const lastMeeting = ref(null);
|
|
|
|
// 점수 통계
|
|
const scoreStats = ref({
|
|
topGame: 0,
|
|
averageScore: 0,
|
|
totalGames: 0
|
|
});
|
|
|
|
// 상위 회원 목록
|
|
const topMembers = ref([]);
|
|
|
|
// 최근 모임 목록
|
|
const recentMeetings = ref([]);
|
|
|
|
// 날짜 포맷 함수
|
|
const formatDate = (date) => {
|
|
return dayjs(date).format('YYYY-MM-DD HH:mm');
|
|
};
|
|
|
|
// 템플릿 함수들
|
|
const rankTemplate = (rowData, column) => {
|
|
return rowData.rank || topMembers.value.indexOf(rowData) + 1;
|
|
};
|
|
|
|
const averageTemplate = (rowData) => {
|
|
return rowData.average ? rowData.average.toFixed(1) : '0.0';
|
|
};
|
|
|
|
const dateTemplate = (rowData) => {
|
|
return formatDate(rowData.date);
|
|
};
|
|
|
|
// 데이터 로드 함수
|
|
const loadDashboardData = async () => {
|
|
if (!clubId.value) {
|
|
console.error('클럽 ID가 없습니다.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
loading.value = true;
|
|
|
|
// 회원 통계 로드
|
|
const membersResponse = await apiClient.post('/api/club/members/stats', {
|
|
clubId: clubId.value
|
|
});
|
|
memberStats.value = membersResponse.data;
|
|
|
|
// 다음 모임 정보 로드 (여러 개)
|
|
const nextMeetingResponse = await apiClient.post('/api/club/meetings/next', {
|
|
clubId: clubId.value
|
|
});
|
|
const data = nextMeetingResponse.data;
|
|
nextMeetings.value = Array.isArray(data) ? data : (data ? [data] : []);
|
|
|
|
// 최근 모임 정보 로드
|
|
const lastMeetingResponse = await apiClient.post('/api/club/meetings/last', {
|
|
clubId: clubId.value
|
|
});
|
|
lastMeeting.value = lastMeetingResponse.data;
|
|
|
|
// 점수 통계 로드
|
|
const scoreStatsResponse = await apiClient.post('/api/club/scores/stats', {
|
|
clubId: clubId.value
|
|
});
|
|
scoreStats.value = scoreStatsResponse.data;
|
|
|
|
// 상위 회원 목록 로드
|
|
const topMembersResponse = await apiClient.post('/api/club/members/top', {
|
|
clubId: clubId.value
|
|
});
|
|
topMembers.value = topMembersResponse.data;
|
|
|
|
// 최근 모임 목록 로드
|
|
const recentMeetingsResponse = await apiClient.post('/api/club/meetings/recent', {
|
|
clubId: clubId.value
|
|
});
|
|
recentMeetings.value = recentMeetingsResponse.data;
|
|
|
|
} catch (error) {
|
|
console.error('대시보드 데이터 로딩 오류:', error);
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: '오류',
|
|
detail: '대시보드 데이터를 불러오는 중 오류가 발생했습니다.',
|
|
life: 3000
|
|
});
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
// 페이지 이동 함수
|
|
const navigateTo = (path) => {
|
|
// Google Analytics 이벤트 추적
|
|
gtag.event('navigation', {
|
|
'event_category': '대시보드',
|
|
'event_label': path,
|
|
'value': 1
|
|
});
|
|
router.push(path);
|
|
};
|
|
|
|
// 컴포넌트 마운트 시 데이터 로드
|
|
onMounted(async () => {
|
|
clubId.value = localStorage.getItem('clubId') || null;
|
|
await loadDashboardData();
|
|
|
|
// Google Analytics 페이지뷰 추적
|
|
gtag.pageview({
|
|
page_title: '클럽 대시보드',
|
|
page_path: window.location.pathname,
|
|
page_location: window.location.href
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.club-dashboard {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.dashboard-card {
|
|
background: #ffffff;
|
|
border-radius: 10px;
|
|
padding: 1rem;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.card-header i {
|
|
font-size: 1.5rem;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.card-header h3 {
|
|
margin: 0;
|
|
color: var(--text-color);
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.card-content {
|
|
flex: 1;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.card-content p {
|
|
margin: 0.5rem 0;
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
.card-footer {
|
|
margin-top: auto;
|
|
}
|
|
</style>
|