대시보드, 통계 정리

This commit is contained in:
2025-06-09 15:35:05 +09:00
parent 141a0de6d0
commit 05312c7d93
4 changed files with 321 additions and 182 deletions
+86 -66
View File
@@ -8,7 +8,7 @@
<div v-else class="grid">
<!-- 회원 통계 카드 -->
<div class="col-12 md:col-6 lg:col-3">
<div class="col-12 md:col-6 lg:col-6">
<div class="dashboard-card">
<div class="card-header">
<i class="pi pi-users"></i>
@@ -24,51 +24,8 @@
</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-calendar"></i>
<h3>다음 모임</h3>
</div>
<div class="card-content" v-if="nextMeeting">
<p>일시: {{ formatDate(nextMeeting.date) }}</p>
<p>장소: {{ nextMeeting.location || '미정' }}</p>
<p>참가 예정: {{ nextMeeting.participantsCount }}</p>
</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>
<!-- 최근 모임 통계 카드 -->
<div class="col-12 md:col-6 lg:col-3">
<div class="dashboard-card">
<div class="card-header">
<i class="pi pi-chart-bar"></i>
<h3>최근 모임</h3>
</div>
<div class="card-content" v-if="lastMeeting">
<p>일시: {{ formatDate(lastMeeting.date) }}</p>
<p>참가자: {{ lastMeeting.participantsCount }}</p>
<p>게임 : {{ lastMeeting.games }}게임</p>
</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>
<!-- 점수 통계 카드 -->
<div class="col-12 md:col-6 lg:col-3">
<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>
@@ -84,17 +41,94 @@
</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 md:col-6">
<div class="col-12">
<div class="dashboard-card">
<div class="card-header">
<h3>에버리지 순위</h3>
</div>
<DataTable :value="topMembers" :rows="5" stripedRows responsiveLayout="scroll">
<Column field="rank" header="#" :body="rankTemplate" style="width: 10%"></Column>
<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>
@@ -102,21 +136,6 @@
</DataTable>
</div>
</div>
<!-- 최근 모임 목록 -->
<div class="col-12 md:col-6">
<div class="dashboard-card">
<div class="card-header">
<h3>최근 모임</h3>
</div>
<DataTable :value="recentMeetings" :rows="5" stripedRows responsiveLayout="scroll">
<Column field="date" header="일시" :body="dateTemplate" style="width: 25%"></Column>
<Column field="title" header="제목" style="width: 40%"></Column>
<Column field="participants" header="참가자" style="width: 15%"></Column>
<Column field="games" header="게임 수" style="width: 20%"></Column>
</DataTable>
</div>
</div>
</div>
</div>
</template>
@@ -142,8 +161,8 @@ const memberStats = ref({
associateCount: 0
});
// 다음 모임 정보
const nextMeeting = ref(null);
// 다음 모임 정보 (여러 개)
const nextMeetings = ref([]);
// 최근 모임 정보
const lastMeeting = ref(null);
@@ -195,11 +214,12 @@ const loadDashboardData = async () => {
});
memberStats.value = membersResponse.data;
// 다음 모임 정보 로드
// 다음 모임 정보 로드 (여러 개)
const nextMeetingResponse = await apiClient.post('/api/club/meetings/next', {
clubId: clubId.value
});
nextMeeting.value = nextMeetingResponse.data;
const data = nextMeetingResponse.data;
nextMeetings.value = Array.isArray(data) ? data : (data ? [data] : []);
// 최근 모임 정보 로드
const lastMeetingResponse = await apiClient.post('/api/club/meetings/last', {
+176 -74
View File
@@ -106,75 +106,78 @@
</div>
</div>
<!-- ===== 추가 통계 표시 영역 ===== -->
<!-- 1. 회원별 최고/최저/총점 -->
<h3>회원별 최고/최저/총점</h3>
<table>
<thead>
<tr><th>이름</th><th>최고점</th><th>최저점</th><th>총점</th></tr>
</thead>
<tbody>
<tr v-for="m in stats.memberScoreStats" :key="m.memberId">
<td>{{ m.name }}</td>
<td>{{ m.maxScore }}</td>
<td>{{ m.minScore }}</td>
<td>{{ m.totalScore }}</td>
</tr>
</tbody>
</table>
<!-- 2. 모임별 최고/최저/평균점수 -->
<h3>모임별 점수 통계(확장)</h3>
<table>
<thead>
<tr><th>모임명</th><th>최고점</th><th>최저점</th><th>평균점</th></tr>
</thead>
<tbody>
<tr v-for="e in stats.meetingScoreStats" :key="e.eventId">
<td>{{ e.title }}</td>
<td>{{ e.maxScore }}</td>
<td>{{ e.minScore }}</td>
<td>{{ e.avgScore }}</td>
</tr>
</tbody>
</table>
<!-- 4. 참가자 변화 추이 (그래프) -->
<h3>참가자 변화 추이</h3>
<LineChart v-if="participantTrendChartData" :data="participantTrendChartData" />
<!-- 5. 회원별 모임 참가 횟수 -->
<h3>회원별 모임 참가 횟수</h3>
<table>
<thead>
<tr><th>이름</th><th>참가 횟수</th></tr>
</thead>
<tbody>
<tr v-for="m in stats.memberAttendanceStats" :key="m.memberId">
<td>{{ m.name }}</td>
<td>{{ m.attendCount }}</td>
</tr>
</tbody>
</table>
<!-- 6. 팀별 통계 -->
<h3>팀별 통계</h3>
<table>
<thead>
<tr><th>이벤트ID</th><th>팀ID</th><th>평균점수</th><th>최고점</th><th>참가자수</th></tr>
</thead>
<tbody>
<tr v-for="t in stats.teamStats" :key="`${t.eventId}-${t.teamId}`">
<td>{{ t.eventId }}</td>
<td>{{ t.teamId }}</td>
<td>{{ t.avgScore }}</td>
<td>{{ t.maxScore }}</td>
<td>{{ t.memberCount }}</td>
</tr>
</tbody>
</table>
<!-- ===== 추가 통계 표시 영역 (정리) ===== -->
<div class="continued-stats-container">
<div class="continued-stats-card">
<h3>회원별 최고/최저/총점</h3>
<table>
<thead>
<tr><th>이름</th><th>최고점</th><th>최저점</th><th>총점</th></tr>
</thead>
<tbody>
<tr v-for="m in stats.memberScoreStats" :key="m.memberId">
<td>{{ m.name }}</td>
<td>{{ m.maxScore }}</td>
<td>{{ m.minScore }}</td>
<td>{{ m.totalScore }}</td>
</tr>
</tbody>
</table>
</div>
<div class="continued-stats-card">
<h3>모임별 점수 통계(확장)</h3>
<table>
<thead>
<tr><th>모임명</th><th>최고점</th><th>최저점</th><th>평균점</th></tr>
</thead>
<tbody>
<tr v-for="e in stats.meetingScoreStats" :key="e.eventId">
<td>{{ e.title }}</td>
<td>{{ e.maxScore }}</td>
<td>{{ e.minScore }}</td>
<td>{{ e.avgScore }}</td>
</tr>
</tbody>
</table>
</div>
<div class="continued-stats-card">
<h3>참가자 변화 추이</h3>
<LineChart v-if="participantTrendChartData" :data="participantTrendChartData" />
</div>
<div class="continued-stats-card">
<h3>회원별 모임 참가 횟수</h3>
<table>
<thead>
<tr><th>이름</th><th>참가 횟수</th></tr>
</thead>
<tbody>
<tr v-for="m in stats.memberAttendanceStats" :key="m.memberId">
<td>{{ m.name }}</td>
<td>{{ m.attendCount }}</td>
</tr>
</tbody>
</table>
</div>
<div class="continued-stats-card">
<h3>팀별 통계</h3>
<table>
<thead>
<tr><th>이벤트ID</th><th>팀ID</th><th>평균점수</th><th>최고점</th><th>참가자수</th></tr>
</thead>
<tbody>
<tr v-for="t in stats.teamStats" :key="`${t.eventId}-${t.teamId}`">
<td>{{ t.eventId }}</td>
<td>{{ t.teamId }}</td>
<td>{{ t.avgScore }}</td>
<td>{{ t.maxScore }}</td>
<td>{{ t.memberCount }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- ===== 추가 통계 표시 영역 ===== -->
</div>
</div>
</template>
@@ -319,9 +322,42 @@ export default {
<style scoped>
.statistics-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
padding: 32px 16px;
}
@media (max-width: 900px) {
.statistics-container {
padding: 12px 2px;
}
.stats-summary,
.rankings-container,
.meetings-container,
.charts-container {
grid-template-columns: 1fr !important;
gap: 12px;
display: block !important;
}
.stat-card, .rankings-card, .meetings-card, .chart-card {
max-width: 100%;
min-width: 0;
}
.stat-card table,
.rankings-card table,
.meetings-card table {
min-width: 320px;
display: block;
overflow-x: auto;
}
table {
min-width: 320px;
font-size: 13px;
}
}
.loading {
display: flex;
flex-direction: column;
@@ -354,20 +390,27 @@ export default {
border: 1px solid #ddd;
}
.stats-summary {
.stats-summary,
.rankings-container,
.meetings-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
min-width: 0;
display: flex;
flex-direction: column;
}
.stat-card h3 {
margin-top: 0;
margin-bottom: 15px;
@@ -376,18 +419,33 @@ export default {
.charts-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: 20px;
margin-bottom: 30px;
width: 100%;
box-sizing: border-box;
}
.chart-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
min-width: 0;
overflow-x: auto;
}
.stat-card table,
.rankings-card table,
.meetings-card table {
width: 100%;
min-width: unset;
display: table;
overflow-x: unset;
}
.chart-card h3 {
margin-top: 0;
margin-bottom: 15px;
@@ -409,10 +467,16 @@ export default {
table {
width: 100%;
min-width: 400px;
border-collapse: collapse;
margin-top: 10px;
background: #fff;
border-radius: 8px;
overflow-x: auto;
display: block;
}
th, td {
padding: 12px;
text-align: left;
@@ -427,4 +491,42 @@ th {
tr:hover {
background-color: #f9f9f9;
}
.continued-stats-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
gap: 24px;
margin-bottom: 40px;
}
.continued-stats-card {
background: #fff;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(0,0,0,0.06);
padding: 20px;
min-width: 0;
margin-bottom: 0;
display: flex;
flex-direction: column;
}
.continued-stats-card table {
width: 100%;
min-width: unset;
display: table;
overflow-x: unset;
}
@media (max-width: 900px) {
.continued-stats-container {
grid-template-columns: 1fr;
gap: 12px;
}
.continued-stats-card {
padding: 12px;
}
.continued-stats-card table {
min-width: 320px;
display: block;
overflow-x: auto;
}
}
</style>