From 05312c7d93b62c2ebd8d1ba1620f8a865c2f6130 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Mon, 9 Jun 2025 15:35:05 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C,=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/clubController.js | 99 ++++++---- backend/routes/clubRoutes.js | 2 +- frontend/src/views/club/Dashboard.vue | 152 ++++++++------- frontend/src/views/club/Statistics.vue | 250 +++++++++++++++++-------- 4 files changed, 321 insertions(+), 182 deletions(-) diff --git a/backend/controllers/clubController.js b/backend/controllers/clubController.js index 31e5171..a15b56a 100644 --- a/backend/controllers/clubController.js +++ b/backend/controllers/clubController.js @@ -626,45 +626,39 @@ exports.getMemberStats = async (req, res) => { } }; -// 다음 모임 정보 조회 -exports.getNextMeeting = async (req, res) => { +// 다음 모임 정보 여러 개 조회 +exports.getNextMeetings = async (req, res) => { try { const { clubId } = req.body; - if (!clubId) { return res.status(400).json({ message: '클럽 ID가 필요합니다.' }); } - - const nextMeeting = await Event.findOne({ + // 오늘 이후의 예정된 모임 중 status가 active인 것 3개까지 오름차순 정렬 + const nextMeetings = await Event.findAll({ where: { clubId, startDate: { [Op.gt]: new Date() }, - status: 'active' + status: { [Op.or]: ['ready', 'active'] } }, include: [{ model: EventParticipant, where: { - status: { [Op.ne]: '취소' } + status: { [Op.ne]: 'canceled' } }, required: false }], - order: [['startDate', 'ASC']] + order: [['startDate', 'ASC']], + limit: 3 }); - - if (!nextMeeting) { - return res.json(null); - } - - const meetingData = { - date: nextMeeting.startDate, - participantsCount: nextMeeting.participants ? nextMeeting.participants.length : 0, - title: nextMeeting.title, - location: nextMeeting.location - }; - - res.json(meetingData); + const meetings = nextMeetings.map(meeting => ({ + date: meeting.startDate, + participantsCount: meeting.EventParticipants ? meeting.EventParticipants.length : 0, + title: meeting.title, + location: meeting.location + })); + res.json(meetings); } catch (error) { - console.error('다음 모임 정보 조회 오류:', error); + console.error('다음 모임 정보(여러 개) 조회 오류:', error); res.status(500).json({ message: '다음 모임 정보를 가져오는 중 오류가 발생했습니다.' }); } }; @@ -687,7 +681,7 @@ exports.getLastMeeting = async (req, res) => { include: [{ model: EventParticipant, where: { - status: { [Op.ne]: '취소' } + status: { [Op.ne]: 'canceled' } }, required: false }], @@ -700,7 +694,7 @@ exports.getLastMeeting = async (req, res) => { const meetingData = { date: lastMeeting.startDate, - participantsCount: lastMeeting.participants ? lastMeeting.participants.length : 0, + participantsCount: lastMeeting.EventParticipants ? lastMeeting.EventParticipants.length : 0, title: lastMeeting.title, games: lastMeeting.gameCount || 0 }; @@ -743,10 +737,10 @@ exports.getScoreStats = async (req, res) => { let totalGames = 0; events.forEach(event => { - event.participants.forEach(participant => { - if (participant.scores && participant.scores.length > 0) { - allScores = allScores.concat(participant.scores.map(score => score.score)); - totalGames += participant.scores.length; + (event.EventParticipants || []).forEach(participant => { + if (participant.EventScores && participant.EventScores.length > 0) { + allScores = allScores.concat(participant.EventScores.map(score => score.score)); + totalGames += participant.EventScores.length; } }); }); @@ -779,31 +773,55 @@ exports.getTopMembers = async (req, res) => { return res.status(404).json({ message: '클럽을 찾을 수 없습니다.' }); } - // 활동적인 회원들 가져오기 (게임 수로 정렬) - const activeMembers = await Member.findAll({ + // 활동 회원들 전체 조회 (games 컬럼 제거) + const members = await Member.findAll({ where: { clubId, - status: 'active', - games: { [Op.gt]: 0 } // 최소 1게임 이상 플레이한 회원 + status: 'active' }, - attributes: ['id', 'name', 'games', 'handicap', 'memberType'], - order: [['games', 'DESC']], - limit: 20 // 에버리지 계산을 위해 여유있게 가져오기 + attributes: ['id', 'name', 'handicap', 'memberType'], + limit: 100 // 충분히 넉넉하게 조회 }); - + + // 각 회원별 실제 게임 수 집계 (EventScore → EventParticipant 조인) + const memberIds = members.map(m => m.id); + // 1. 회원별 EventParticipant 조회 + const participants = await EventParticipant.findAll({ + where: { memberId: memberIds }, + attributes: ['id', 'memberId'] + }); + const participantIdToMemberId = {}; + participants.forEach(p => { participantIdToMemberId[p.id] = p.memberId; }); + // 2. EventScore에서 참가자별 점수 조회 + const scores = await EventScore.findAll({ + where: { participantId: Object.keys(participantIdToMemberId) }, + attributes: ['participantId'] + }); + // 3. memberId별로 게임수 카운트 + const gamesMap = {}; + scores.forEach(s => { + const memberId = participantIdToMemberId[s.participantId]; + gamesMap[memberId] = (gamesMap[memberId] || 0) + 1; + }); + + // 1게임 이상 플레이한 회원만 필터링 + const filteredMembers = members.filter(m => (gamesMap[m.id] || 0) > 0); + // 각 회원의 에버리지 계산 const membersWithAverage = await Promise.all( - activeMembers.map(async (member) => { + filteredMembers.map(async (member) => { try { const averageData = await averageService.calculateMemberAverageByClubSetting(member.id, clubId); return { ...member.toJSON(), + games: gamesMap[member.id] || 0, average: averageData.average }; } catch (error) { console.error(`회원 ${member.id}의 에버리지 계산 오류:`, error); return { ...member.toJSON(), + games: gamesMap[member.id] || 0, average: 0 }; } @@ -834,24 +852,23 @@ exports.getRecentMeetings = async (req, res) => { const recentMeetings = await Event.findAll({ where: { clubId, - status: 'active', + status: { [Op.notIn]: ['canceled', 'deleted'] }, startDate: { [Op.lt]: new Date() } }, include: [{ model: EventParticipant, where: { - status: { [Op.ne]: '취소' } + status: { [Op.ne]: 'canceled' } }, required: false }], order: [['startDate', 'DESC']], limit: 5 }); - const formattedMeetings = recentMeetings.map(meeting => ({ date: meeting.startDate, title: meeting.title, - participants: meeting.participants ? meeting.participants.length : 0, + participantsCount: meeting.EventParticipants ? meeting.EventParticipants.length : 0, games: meeting.gameCount || 0 })); diff --git a/backend/routes/clubRoutes.js b/backend/routes/clubRoutes.js index 3a5ac6e..923975c 100644 --- a/backend/routes/clubRoutes.js +++ b/backend/routes/clubRoutes.js @@ -39,7 +39,7 @@ router.post('/members/update', authenticateJWT, clubController.updateClubMember) router.post('/members/delete', authenticateJWT, clubController.deleteClubMember); // 다음 모임 정보 조회 -router.post('/meetings/next', authenticateJWT, clubController.getNextMeeting); +router.post('/meetings/next', authenticateJWT, clubController.getNextMeetings); // 최근 모임 정보 조회 router.post('/meetings/last', authenticateJWT, clubController.getLastMeeting); diff --git a/frontend/src/views/club/Dashboard.vue b/frontend/src/views/club/Dashboard.vue index b4ab6b7..a0eebe5 100644 --- a/frontend/src/views/club/Dashboard.vue +++ b/frontend/src/views/club/Dashboard.vue @@ -8,7 +8,7 @@
-
+
@@ -24,51 +24,8 @@
- - -
-
-
- -

다음 모임

-
-
-

일시: {{ formatDate(nextMeeting.date) }}

-

장소: {{ nextMeeting.location || '미정' }}

-

참가 예정: {{ nextMeeting.participantsCount }}명

-
-
-

예정된 모임이 없습니다.

-
- -
-
- - -
-
-
- -

최근 모임

-
-
-

일시: {{ formatDate(lastMeeting.date) }}

-

참가자: {{ lastMeeting.participantsCount }}명

-

게임 수: {{ lastMeeting.games }}게임

-
-
-

최근 모임 기록이 없습니다.

-
- -
-
- -
+
@@ -84,17 +41,94 @@
+ +
+
+
+ +

다음 모임

+
+
+ + + + + + + + + + + + + + + + + +
일시제목장소참가자
{{ formatDate(meeting.date) }}{{ meeting.title || '-' }}{{ meeting.location || '미정' }}{{ meeting.participantsCount || meeting.participants || 0 }}명
+
+
+

예정된 모임이 없습니다.

+
+ +
+
+ + +
+
+
+ +

최근 모임

+
+
+ + + + + + + + + + + + + + + + + + + + +
일시제목참가자게임 수
{{ formatDate(meeting.date) }}{{ meeting.title }}{{ meeting.participantsCount || meeting.participants || 0 }}명{{ meeting.games }}게임
최근 모임 데이터가 없습니다.
+
+ +
+
+
-
+

에버리지 순위

- + + @@ -102,21 +136,6 @@
- - -
-
-
-

최근 모임

-
- - - - - - -
-
@@ -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', { diff --git a/frontend/src/views/club/Statistics.vue b/frontend/src/views/club/Statistics.vue index 6959766..7802594 100644 --- a/frontend/src/views/club/Statistics.vue +++ b/frontend/src/views/club/Statistics.vue @@ -106,75 +106,78 @@
- - -

회원별 최고/최저/총점

- - - - - - - - - - - - -
이름최고점최저점총점
{{ m.name }}{{ m.maxScore }}{{ m.minScore }}{{ m.totalScore }}
- - -

모임별 점수 통계(확장)

- - - - - - - - - - - - -
모임명최고점최저점평균점
{{ e.title }}{{ e.maxScore }}{{ e.minScore }}{{ e.avgScore }}
- - -

참가자 수 변화 추이

- - - -

회원별 모임 참가 횟수

- - - - - - - - - - -
이름참가 횟수
{{ m.name }}{{ m.attendCount }}
- - -

팀별 통계

- - - - - - - - - - - - - -
이벤트ID팀ID평균점수최고점참가자수
{{ t.eventId }}{{ t.teamId }}{{ t.avgScore }}{{ t.maxScore }}{{ t.memberCount }}
- + +
+
+

회원별 최고/최저/총점

+ + + + + + + + + + + + +
이름최고점최저점총점
{{ m.name }}{{ m.maxScore }}{{ m.minScore }}{{ m.totalScore }}
+
+
+

모임별 점수 통계(확장)

+ + + + + + + + + + + + +
모임명최고점최저점평균점
{{ e.title }}{{ e.maxScore }}{{ e.minScore }}{{ e.avgScore }}
+
+
+

참가자 수 변화 추이

+ +
+
+

회원별 모임 참가 횟수

+ + + + + + + + + + +
이름참가 횟수
{{ m.name }}{{ m.attendCount }}
+
+
+

팀별 통계

+ + + + + + + + + + + + + +
이벤트ID팀ID평균점수최고점참가자수
{{ t.eventId }}{{ t.teamId }}{{ t.avgScore }}{{ t.maxScore }}{{ t.memberCount }}
+
+
+ @@ -319,9 +322,42 @@ export default {