This commit is contained in:
2025-04-17 23:32:39 +09:00
commit 5b719b6766
105 changed files with 25653 additions and 0 deletions
+338
View File
@@ -0,0 +1,338 @@
<template>
<div class="statistics-container">
<div v-if="loading" class="loading">
<div class="spinner"></div>
<p>통계 데이터를 불러오는 ...</p>
</div>
<div v-else>
<div class="period-selector">
<select v-model="selectedPeriod" @change="loadStatistics">
<option value="30">최근 30</option>
<option value="90">최근 90</option>
<option value="180">최근 180</option>
<option value="365">최근 1</option>
</select>
</div>
<div class="stats-summary">
<div class="stat-card">
<h3>회원 현황</h3>
<p>전체 회원: {{ stats.summary.totalMembers }}</p>
<p>활동 회원: {{ stats.summary.activeMembers }}</p>
<p>평균 참석률: {{ stats.summary.averageAttendance }}%</p>
</div>
<div class="stat-card">
<h3>점수 현황</h3>
<p>최고 점수: {{ stats.summary.highestGame }}</p>
<p>평균 점수: {{ stats.summary.averageScore }}</p>
<p> 게임 : {{ stats.summary.totalGames }}게임</p>
</div>
<div class="stat-card">
<h3>모임 현황</h3>
<p> 모임 : {{ stats.summary.totalMeetings }}</p>
<p>평균 참가자: {{ stats.summary.averageParticipants }}</p>
<p>모임당 게임 : {{ stats.summary.averageGamesPerMeeting }}게임</p>
</div>
</div>
<div class="charts-container">
<div class="chart-card">
<h3>점수 분포</h3>
<BarChart
:data="scoreDistributionChartData"
:options="scoreDistributionChartOptions"
/>
</div>
<div class="chart-card">
<h3>참석률 추이</h3>
<LineChart
:data="attendanceTrendChartData"
:options="attendanceTrendChartOptions"
/>
</div>
</div>
<div class="rankings-container">
<div class="rankings-card">
<h3>회원 순위</h3>
<table>
<thead>
<tr>
<th>순위</th>
<th>이름</th>
<th>평균</th>
<th>게임 </th>
<th>핸디캡</th>
<th>참석률</th>
</tr>
</thead>
<tbody>
<tr v-for="(member, index) in stats.memberRankings" :key="member.id">
<td>{{ index + 1 }}</td>
<td>{{ member.name }}</td>
<td>{{ member.average }}</td>
<td>{{ member.games }}</td>
<td>{{ member.handicap }}</td>
<td>{{ member.attendance }}%</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="meetings-container">
<div class="meetings-card">
<h3>모임별 통계</h3>
<table>
<thead>
<tr>
<th>날짜</th>
<th>제목</th>
<th>참가자</th>
<th>게임 </th>
<th>평균 점수</th>
</tr>
</thead>
<tbody>
<tr v-for="meeting in stats.meetingStats" :key="meeting.date">
<td>{{ formatDate(meeting.date) }}</td>
<td>{{ meeting.title }}</td>
<td>{{ meeting.participants }}</td>
<td>{{ meeting.games }}게임</td>
<td>{{ meeting.averageScore }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, computed } from 'vue';
import { Bar as BarChart, Line as LineChart } from 'vue-chartjs';
import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend } from 'chart.js';
import apiClient from '@/services/api';
ChartJS.register(CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend);
export default {
name: 'Statistics',
components: {
BarChart,
LineChart
},
setup() {
const stats = ref({
summary: {},
scoreDistribution: [],
attendanceTrend: [],
memberRankings: [],
meetingStats: []
});
const selectedPeriod = ref('90');
const loading = ref(false);
const loadStatistics = async () => {
loading.value = true;
try {
const clubId = localStorage.getItem('clubId');
const response = await apiClient.post('/api/club/statistics', {
clubId,
days: parseInt(selectedPeriod.value)
});
stats.value = response.data;
} catch (error) {
console.error('통계 데이터 로드 오류:', error);
} finally {
loading.value = false;
}
};
const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
};
const scoreDistributionChartData = computed(() => ({
labels: stats.value.scoreDistribution?.map(d => d.range) || [],
datasets: [{
label: '게임 수',
data: stats.value.scoreDistribution?.map(d => d.count) || [],
backgroundColor: '#4CAF50'
}]
}));
const scoreDistributionChartOptions = {
responsive: true,
plugins: {
legend: {
display: false
},
title: {
display: false
}
}
};
const attendanceTrendChartData = computed(() => ({
labels: stats.value.attendanceTrend?.map(d => formatDate(d.date)) || [],
datasets: [{
label: '참석률',
data: stats.value.attendanceTrend?.map(d => d.attendance) || [],
borderColor: '#2196F3',
tension: 0.1
}]
}));
const attendanceTrendChartOptions = {
responsive: true,
plugins: {
legend: {
display: false
}
},
scales: {
y: {
beginAtZero: true,
max: 100
}
}
};
onMounted(() => {
loadStatistics();
});
return {
stats,
selectedPeriod,
loading,
loadStatistics,
formatDate,
scoreDistributionChartData,
scoreDistributionChartOptions,
attendanceTrendChartData,
attendanceTrendChartOptions
};
}
};
</script>
<style scoped>
.statistics-container {
padding: 20px;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.period-selector {
margin-bottom: 20px;
}
.period-selector select {
padding: 8px;
border-radius: 4px;
border: 1px solid #ddd;
}
.stats-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-card h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
}
.charts-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.chart-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.chart-card h3 {
margin-top: 0;
margin-bottom: 15px;
color: #333;
}
.rankings-container,
.meetings-container {
margin-bottom: 30px;
}
.rankings-card,
.meetings-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #f5f5f5;
font-weight: 600;
}
tr:hover {
background-color: #f9f9f9;
}
</style>