오류 수정, 통계 보강
This commit is contained in:
@@ -19,8 +19,8 @@
|
||||
<TabPanel value="기본정보">
|
||||
<div class="event-details p-3">
|
||||
<div class="flex align-items-center gap-2 mb-3">
|
||||
<Badge :value="getEventTypeName(eventModel.eventType)" :severity="getStatusSeverity(eventModel.status)" />
|
||||
<Badge :value="getStatusName(eventModel.status)" :severity="getStatusSeverity(eventModel.status)" />
|
||||
<Badge :value="getEventTypeLabel(eventModel.eventType)" :severity="getEventTypeSeverity(eventModel.eventType)" />
|
||||
<Badge :value="getEventStatusLabel(eventModel.status)" :severity="getStatusSeverity(eventModel.status)" />
|
||||
</div>
|
||||
<div class="card surface-100 p-3 mb-3">
|
||||
<div class="grid">
|
||||
@@ -92,6 +92,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getEventTypeLabel, getEventStatusLabel } from '@/utils/enumMappings';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import TeamGeneration from './TeamGeneration.vue';
|
||||
import Dialog from 'primevue/dialog';
|
||||
@@ -101,8 +102,8 @@ import ScoreManagement from './ScoreManagement.vue';
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, required: true },
|
||||
event: { type: Object, default: () => ({}) },
|
||||
getEventTypeName: { type: Function, required: true },
|
||||
getStatusName: { type: Function, required: true },
|
||||
// getEventTypeName, getStatusName prop 제거 (enumMappings.js 함수로 일원화)
|
||||
getEventTypeSeverity: { type: Function, required: true },
|
||||
getStatusSeverity: { type: Function, required: true },
|
||||
formatDate: { type: Function, required: true },
|
||||
// 팀 기능 사용 권한 여부
|
||||
@@ -157,7 +158,7 @@ watch(() => eventModel.value.id, async (eventId) => {
|
||||
// 참가확정 참가자 목록을 계산하는 computed 속성
|
||||
const confirmedParticipants = ref([]);
|
||||
function updateConfirmedParticipants() {
|
||||
const arr = eventModel.value.EventParticipants?.filter(p => p.status === '참가확정' || p.status === '참가예정') || [];
|
||||
const arr = eventModel.value.EventParticipants?.filter(p => p.status === 'confirmed' || p.status === 'pending') || [];
|
||||
confirmedParticipants.value = arr;
|
||||
}
|
||||
// 최초 및 eventModel 변경 시 동기화
|
||||
|
||||
@@ -21,13 +21,13 @@
|
||||
|
||||
<div class="col-12 md:col-6">
|
||||
<label for="eventType">이벤트 유형 *</label>
|
||||
<Select id="eventType" v-model="eventModel.eventType" variant="filled" :options="eventTypeOptions" optionLabel="name" optionValue="value" placeholder="유형 선택" :class="{'p-invalid': submitted && !eventModel.eventType, 'w-full': true}"/>
|
||||
<Select id="eventType" v-model="eventModel.eventType" variant="filled" :options="EVENT_TYPE_OPTIONS" optionLabel="name" optionValue="value" placeholder="유형 선택" :class="{'p-invalid': submitted && !eventModel.eventType, 'w-full': true}"/>
|
||||
<small class="p-error" v-if="submitted && !eventModel.eventType">이벤트 유형은 필수입니다.</small>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-6">
|
||||
<label for="status">상태 *</label>
|
||||
<Select id="status" v-model="eventModel.status" :options="statusOptions" optionLabel="name" optionValue="value" placeholder="상태 선택" :class="{'p-invalid': submitted && !eventModel.status, 'w-full': true}" />
|
||||
<Select id="status" v-model="eventModel.status" :options="EVENT_STATUS_OPTIONS" optionLabel="name" optionValue="value" placeholder="상태 선택" :class="{'p-invalid': submitted && !eventModel.status, 'w-full': true}" />
|
||||
<small class="p-error" v-if="submitted && !eventModel.status">상태는 필수입니다.</small>
|
||||
</div>
|
||||
|
||||
@@ -96,17 +96,17 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, toRefs, computed } from 'vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import InputGroup from 'primevue/inputgroup';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const statusOptions = [
|
||||
{ name: '준비', value: '준비' },
|
||||
{ name: '활성', value: '활성' },
|
||||
{ name: '완료', value: '완료' },
|
||||
{ name: '취소', value: '취소' },
|
||||
{ name: '삭제', value: '삭제' }
|
||||
];
|
||||
import {
|
||||
EVENT_TYPE_OPTIONS,
|
||||
EVENT_STATUS_OPTIONS,
|
||||
getEventTypeLabel,
|
||||
getEventStatusLabel,
|
||||
getEventTypeSeverity,
|
||||
getStatusSeverity
|
||||
} from '@/utils/enumMappings';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
@@ -129,18 +129,12 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['update:visible', 'save', 'hide']);
|
||||
|
||||
// 이벤트 타입 옵션
|
||||
const eventTypeOptions = [
|
||||
{ name: '정기전', value: '정기전' },
|
||||
{ name: '번개', value: '번개' },
|
||||
{ name: '연습', value: '연습' },
|
||||
{ name: '교류전', value: '교류전' },
|
||||
{ name: '이벤트', value: '이벤트' },
|
||||
{ name: '기타', value: '기타' }
|
||||
];
|
||||
|
||||
// 이벤트 모델
|
||||
const eventModel = ref({ ...props.event });
|
||||
const eventModel = ref({
|
||||
...props.event,
|
||||
status: props.event.status ?? 'ready',
|
||||
eventType: props.event.eventType ?? 'regular'
|
||||
});
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
const toast = useToast();
|
||||
const loadingHash = ref(false);
|
||||
@@ -158,7 +152,9 @@ watch(() => props.event, (newValue) => {
|
||||
...newValue,
|
||||
startDate: formatDate(newValue.startDate),
|
||||
endDate: formatDate(newValue.endDate),
|
||||
registrationDeadline: formatDate(newValue.registrationDeadline)
|
||||
registrationDeadline: formatDate(newValue.registrationDeadline),
|
||||
status: newValue.status ?? 'ready',
|
||||
eventType: newValue.eventType ?? 'regular'
|
||||
};
|
||||
eventModel.value = formattedEvent;
|
||||
}, { deep: true });
|
||||
|
||||
@@ -251,7 +251,7 @@
|
||||
<Select
|
||||
id="eventType"
|
||||
v-model="eventData.eventType"
|
||||
:options="eventTypeOptions"
|
||||
:options="EVENT_TYPE_OPTIONS"
|
||||
optionLabel="name"
|
||||
optionValue="value"
|
||||
placeholder="이벤트 유형 선택"
|
||||
@@ -359,7 +359,7 @@
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">유형:</span>
|
||||
<span class="summary-value">{{ getEventTypeName(eventData.eventType) }}</span>
|
||||
<span class="summary-value">{{ getEventTypeLabel(eventData.eventType) }}</span>
|
||||
</div>
|
||||
<div class="summary-item">
|
||||
<span class="summary-label">시작일:</span>
|
||||
@@ -493,12 +493,12 @@
|
||||
|
||||
<div class="field col-6 mb-3">
|
||||
<label for="newMemberGender" class="mb-1">성별</label>
|
||||
<Select id="newMemberGender" v-model="newMember.gender" :options="genderOptions" optionLabel="name" optionValue="code" placeholder="성별 선택" class="w-full" />
|
||||
<Select id="newMemberGender" v-model="newMember.gender" :options="GENDER_OPTIONS" optionLabel="name" optionValue="value" placeholder="성별 선택" class="w-full" />
|
||||
</div>
|
||||
|
||||
<div class="field col-6 mb-3">
|
||||
<label for="newMemberType" class="mb-1">회원 유형</label>
|
||||
<Select id="newMemberType" v-model="newMember.memberType" :options="memberTypeOptions" optionLabel="name" optionValue="code" placeholder="회원 유형 선택" class="w-full" :disabled="newMemberMode === 'guest'" />
|
||||
<Select id="newMemberType" v-model="newMember.memberType" :options="MEMBER_TYPE_OPTIONS" optionLabel="name" optionValue="code" placeholder="회원 유형 선택" class="w-full" :disabled="newMemberMode === 'guest'" />
|
||||
</div>
|
||||
|
||||
<div class="field col-6 mb-3">
|
||||
@@ -518,7 +518,7 @@
|
||||
|
||||
<div class="field col-6 mb-3">
|
||||
<label for="newMemberStatus" class="mb-1">상태</label>
|
||||
<Select id="newMemberStatus" v-model="newMember.status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="상태 선택" class="w-full" />
|
||||
<Select id="newMemberStatus" v-model="newMember.status" :options="STATUS_OPTIONS" optionLabel="label" optionValue="value" placeholder="상태 선택" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -530,8 +530,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import {
|
||||
getEventTypeLabel,
|
||||
getStatusLabel,
|
||||
getPaymentStatusLabel,
|
||||
getMemberTypeLabel,
|
||||
getGenderLabel,
|
||||
EVENT_TYPE_OPTIONS,
|
||||
STATUS_OPTIONS,
|
||||
PAYMENT_STATUS_OPTIONS,
|
||||
MEMBER_TYPE_OPTIONS,
|
||||
GENDER_OPTIONS
|
||||
} from '@/utils/enumMappings';
|
||||
import { ref, reactive, computed, watch, nextTick, onMounted } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import dayjs from 'dayjs';
|
||||
import { useClubStore } from '@/stores/clubStore';
|
||||
import eventService from '@/services/eventService';
|
||||
import clubService from '@/services/clubService';
|
||||
@@ -661,37 +674,7 @@ const newMember = ref({
|
||||
status: 'active'
|
||||
});
|
||||
|
||||
// 성별 옵션
|
||||
const genderOptions = [
|
||||
{ name: '남성', code: '남성' },
|
||||
{ name: '여성', code: '여성' }
|
||||
];
|
||||
|
||||
// 회원 유형 옵션
|
||||
const memberTypeOptions = [
|
||||
{ name: '정회원', code: '정회원' },
|
||||
{ name: '준회원', code: '준회원' },
|
||||
{ name: '명예회원', code: '명예회원' },
|
||||
{ name: '운영진', code: '운영진' },
|
||||
{ name: '게스트', code: '게스트' }
|
||||
];
|
||||
|
||||
// 상태 옵션
|
||||
const statusOptions = [
|
||||
{ label: '활성', value: 'active' },
|
||||
{ label: '비활성', value: 'inactive' },
|
||||
{ label: '정지', value: 'suspended' }
|
||||
];
|
||||
|
||||
// 이벤트 유형 옵션
|
||||
const eventTypeOptions = [
|
||||
{ name: '정기전', value: '정기전' },
|
||||
{ name: '번개', value: '번개' },
|
||||
{ name: '연습', value: '연습' },
|
||||
{ name: '교류전', value: '교류전' },
|
||||
{ name: '이벤트', value: '이벤트' },
|
||||
{ name: '기타', value: '기타' }
|
||||
];
|
||||
|
||||
// 점수에 핸디캡 포함 여부
|
||||
const scoresIncludeHandicap = ref(false);
|
||||
@@ -714,7 +697,7 @@ const loadClubMembers = async () => {
|
||||
clubMembers.value = response.data.map(member => ({
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
gender: member.gender,
|
||||
gender: getGenderLabel(member.gender),
|
||||
average: member.average || 0,
|
||||
handicap: member.handicap || 0
|
||||
}));
|
||||
@@ -1098,11 +1081,7 @@ const getUniqueTeamCount = () => {
|
||||
return teams.size;
|
||||
};
|
||||
|
||||
// 이벤트 유형 이름 조회
|
||||
const getEventTypeName = (type) => {
|
||||
const option = eventTypeOptions.find(opt => opt.value === type);
|
||||
return option ? option.name : type;
|
||||
};
|
||||
|
||||
|
||||
// 날짜 포맷
|
||||
const formatDate = (date) => {
|
||||
|
||||
@@ -23,17 +23,17 @@
|
||||
</Column>
|
||||
<Column field="Member.memberType" header="회원 유형" sortable>
|
||||
<template #body="slotProps">
|
||||
<Badge :value="getMemberTypeName(slotProps.data.Member.memberType)" :severity="getMemberTypeSeverity(slotProps.data.Member.memberType)" />
|
||||
<Badge :value="getMemberTypeLabel(slotProps.data.Member.memberType)" :severity="getMemberTypeSeverity(slotProps.data.Member.memberType)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="status" header="참가 상태" sortable>
|
||||
<template #body="slotProps">
|
||||
<Badge :value="getStatusName(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||
<Badge :value="getParticipantStatusLabel(slotProps.data.status)" :severity="getParticipantStatusSeverity(slotProps.data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="paymentStatus" header="결제 상태" sortable>
|
||||
<template #body="slotProps">
|
||||
<Badge :value="getPaymentStatusName(slotProps.data.paymentStatus)" :severity="getPaymentStatusSeverity(slotProps.data.paymentStatus)" />
|
||||
<Badge :value="getPaymentStatusLabel(slotProps.data.paymentStatus)" :severity="getPaymentStatusSeverity(slotProps.data.paymentStatus)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="createdAt" header="등록일" sortable>
|
||||
@@ -72,7 +72,7 @@
|
||||
<Select
|
||||
id="status"
|
||||
v-model="editingParticipant.status"
|
||||
:options="participantStatusOptions"
|
||||
:options="PARTICIPANT_STATUS_OPTIONS"
|
||||
optionLabel="name"
|
||||
optionValue="value"
|
||||
placeholder="상태를 선택하세요"
|
||||
@@ -85,7 +85,7 @@
|
||||
<Select
|
||||
id="paymentStatus"
|
||||
v-model="editingParticipant.paymentStatus"
|
||||
:options="paymentStatusOptions"
|
||||
:options="PAYMENT_STATUS_OPTIONS"
|
||||
optionLabel="name"
|
||||
optionValue="value"
|
||||
placeholder="결제 상태를 선택하세요"
|
||||
@@ -114,6 +114,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
PARTICIPANT_STATUS_OPTIONS,
|
||||
PAYMENT_STATUS_OPTIONS,
|
||||
getMemberTypeLabel,
|
||||
getMemberTypeSeverity,
|
||||
getParticipantStatusLabel,
|
||||
getParticipantStatusSeverity,
|
||||
getPaymentStatusLabel,
|
||||
getPaymentStatusSeverity
|
||||
} from '@/utils/enumMappings';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import eventService from '@/services/eventService';
|
||||
@@ -142,22 +152,11 @@ const clubId = ref(localStorage.getItem('clubId') || null);
|
||||
const editingParticipant = ref({
|
||||
clubId: clubId.value,
|
||||
memberId: null,
|
||||
status: '참가예정',
|
||||
paymentStatus: '미납'
|
||||
status: 'pending',
|
||||
paymentStatus: 'unpaid'
|
||||
});
|
||||
const participantToDelete = ref(null);
|
||||
|
||||
const participantStatusOptions = [
|
||||
{ name: '참가 예정', value: '참가예정' },
|
||||
{ name: '참가 확정', value: '참가확정' },
|
||||
{ name: '취소', value: '취소' }
|
||||
];
|
||||
|
||||
const paymentStatusOptions = [
|
||||
{ name: '미납', value: '미납' },
|
||||
{ name: '납부 완료', value: '납부완료' }
|
||||
];
|
||||
|
||||
const fetchParticipants = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
@@ -179,8 +178,8 @@ const openNewDialog = () => {
|
||||
editingParticipant.value = {
|
||||
clubId: localStorage.getItem('clubId') || null,
|
||||
memberId: null,
|
||||
status: '참가예정',
|
||||
paymentStatus: '미납'
|
||||
status: 'pending',
|
||||
paymentStatus: 'unpaid'
|
||||
};
|
||||
submitted.value = false;
|
||||
participantDialog.value = true;
|
||||
@@ -284,58 +283,6 @@ const formatDate = (date) => {
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm');
|
||||
};
|
||||
|
||||
const getMemberTypeName = (type) => {
|
||||
const types = {
|
||||
regular: '정회원',
|
||||
associate: '준회원',
|
||||
guest: '게스트'
|
||||
};
|
||||
return types[type] || type;
|
||||
};
|
||||
|
||||
const getMemberTypeSeverity = (type) => {
|
||||
const severities = {
|
||||
regular: 'success',
|
||||
associate: 'info',
|
||||
guest: 'warning'
|
||||
};
|
||||
return severities[type] || 'info';
|
||||
};
|
||||
|
||||
const getStatusName = (status) => {
|
||||
const statuses = {
|
||||
참가예정: '참가 예정',
|
||||
참가완료: '참가 완료',
|
||||
취소: '취소'
|
||||
};
|
||||
return statuses[status] || status;
|
||||
};
|
||||
|
||||
const getStatusSeverity = (status) => {
|
||||
const severities = {
|
||||
참가예정: 'warning',
|
||||
참가완료: 'success',
|
||||
취소: 'danger'
|
||||
};
|
||||
return severities[status] || 'info';
|
||||
};
|
||||
|
||||
const getPaymentStatusName = (status) => {
|
||||
const statuses = {
|
||||
미납: '미납',
|
||||
납부완료: '납부 완료'
|
||||
};
|
||||
return statuses[status] || status;
|
||||
};
|
||||
|
||||
const getPaymentStatusSeverity = (status) => {
|
||||
const severities = {
|
||||
미납: 'danger',
|
||||
납부완료: 'success'
|
||||
};
|
||||
return severities[status] || 'info';
|
||||
};
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
return editingParticipant.value.id ? '참가자 정보 수정' : '참가자 추가';
|
||||
});
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<Select
|
||||
v-model="team._selectedMember"
|
||||
:options="availableMembersForTeam(idx)"
|
||||
optionLabel="name"
|
||||
:optionLabel="memberOptionLabel"
|
||||
placeholder="팀원 선택"
|
||||
style="width: 180px;"
|
||||
>
|
||||
@@ -88,6 +88,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 이름(에버리지)로 Select optionLabel 표시
|
||||
function memberOptionLabel(option) {
|
||||
if (!option) return '';
|
||||
const avg = getMemberAverage(option);
|
||||
return avg !== null && avg !== undefined && avg !== ''
|
||||
? `${option.name} (${avg})`
|
||||
: option.name;
|
||||
}
|
||||
|
||||
import draggable from 'vuedraggable'; // npm install vuedraggable@next 필요 (vue3)
|
||||
import { ref, computed, watch, unref } from 'vue';
|
||||
|
||||
@@ -161,7 +170,8 @@ function addMemberToTeam(teamIdx) {
|
||||
if (!team.members.some(m => m.id === member.id)) {
|
||||
team.members.push({ id: member.id, name: member.name });
|
||||
}
|
||||
team._selectedMember = null;
|
||||
// 모든 팀의 셀렉트 선택값을 초기화 (동일인 중복 선택 방지)
|
||||
teams.value.forEach(t => { t._selectedMember = null; });
|
||||
}
|
||||
|
||||
function removeMemberFromTeam(teamIdx, memberIdx) {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<Tag :value="slotProps.value.group" :severity="slotProps.value.group === '참가자' ? 'success' : 'info'" class="mr-2" />
|
||||
<span style="font-weight:bold;">{{ slotProps.value.name }}</span>
|
||||
<span style="color:#888; font-size:0.93em; margin-left: 0.5em;">
|
||||
{{ slotProps.value.memberType }} / {{ slotProps.value.gender }} / 에버리지: {{ slotProps.value.average ?? '-' }}
|
||||
{{ getMemberTypeLabel(slotProps.value.memberType) }} / {{ getGenderLabel(slotProps.value.gender) }} / 에버리지: {{ slotProps.value.average ?? '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<span v-else>
|
||||
@@ -46,7 +46,7 @@
|
||||
<Tag :value="slotProps.option.group" :severity="slotProps.option.group === '참가자' ? 'success' : 'info'" class="mr-2" />
|
||||
<span style="font-weight:bold;">{{ slotProps.option.name }}</span>
|
||||
<span style="color:#888; font-size:0.93em; margin-left: 0.5em;">
|
||||
{{ slotProps.option.memberType }} / {{ slotProps.option.gender }} / 에버리지: {{ slotProps.option.average ?? '-' }}
|
||||
{{ getMemberTypeLabel(slotProps.option.memberType) }} / {{ getGenderLabel(slotProps.option.gender) }} / 에버리지: {{ slotProps.option.average ?? '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label>성별</label>
|
||||
<Select v-model="newGuest.gender" :options="genderOptions" optionLabel="label" optionValue="value" placeholder="성별 선택" class="w-full" />
|
||||
<Select v-model="newGuest.gender" :options="GENDER_OPTIONS" optionLabel="label" optionValue="value" placeholder="성별 선택" class="w-full" />
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label>에버리지</label>
|
||||
@@ -80,7 +80,7 @@
|
||||
<label>참가 상태</label>
|
||||
<Select
|
||||
v-model="status"
|
||||
:options="statusOptions"
|
||||
:options="STATUS_OPTIONS"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
placeholder="상태를 선택하세요"
|
||||
@@ -118,6 +118,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
STATUS_OPTIONS,
|
||||
GENDER_OPTIONS,
|
||||
PAYMENT_STATUS_OPTIONS,
|
||||
getGenderLabel,
|
||||
getMemberTypeLabel,
|
||||
getStatusLabel,
|
||||
getPaymentStatusLabel
|
||||
} from '@/utils/enumMappings';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import Tag from 'primevue/tag';
|
||||
import InputText from 'primevue/inputtext';
|
||||
@@ -198,24 +207,18 @@ const error = ref('');
|
||||
const submitted = ref(false);
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '참가예정', value: '참가예정' },
|
||||
{ label: '참가확정', value: '참가확정' },
|
||||
{ label: '취소', value: '취소' }
|
||||
{ label: getStatusLabel('참가예정'), value: '참가예정' },
|
||||
{ label: getStatusLabel('참가확정'), value: '참가확정' },
|
||||
{ label: getStatusLabel('취소'), value: '취소' }
|
||||
];
|
||||
const paymentStatusOptions = [
|
||||
{ label: '미납', value: '미납' },
|
||||
{ label: '납부완료', value: '납부완료' }
|
||||
{ label: getPaymentStatusLabel('미납'), value: '미납' },
|
||||
{ label: getPaymentStatusLabel('납부완료'), value: '납부완료' }
|
||||
];
|
||||
|
||||
const newGuest = ref({ name: '', phone: '', gender: '' });
|
||||
|
||||
import PublicEventService from '@/services/PublicEventService';
|
||||
|
||||
const newGuest = ref({ name: '', phone: '', gender: '' });
|
||||
const genderOptions = [
|
||||
{ label: '남성', value: '남성' },
|
||||
{ label: '여성', value: '여성' }
|
||||
];
|
||||
|
||||
async function addNewGuest() {
|
||||
error.value = '';
|
||||
if (!newGuest.value.name || !newGuest.value.phone || !newGuest.value.gender) {
|
||||
|
||||
@@ -16,18 +16,9 @@
|
||||
<Tag v-if="member.isGuest" value="G" severity="warning" class="ml-2" />
|
||||
</div>
|
||||
<div class="info-under-name">
|
||||
{{ member.gender || '-' }}
|
||||
<span v-if="member.status" :class="{
|
||||
'status-cancel': member.status === '취소',
|
||||
'status-pending': member.status === '참가예정'
|
||||
}">
|
||||
· {{ member.status }}
|
||||
</span>
|
||||
<span v-if="member.paymentStatus" :class="{
|
||||
'payment-unpaid': member.paymentStatus === '미납'
|
||||
}">
|
||||
· {{ member.paymentStatus }}
|
||||
</span>
|
||||
{{ getGenderLabel(member.gender) || '-' }}
|
||||
<span v-if="member.status" :class="{ 'status-cancel': member.status === 'canceled', 'status-pending': member.status === 'pending' }">· {{ getParticipantStatusLabel(member.status) }}</span>
|
||||
<span v-if="member.paymentStatus" :class="{ 'payment-unpaid': member.paymentStatus === 'unpaid' }">· {{ getPaymentStatusLabel(member.paymentStatus) }}</span>
|
||||
</div>
|
||||
<div class="member-meta">핸디캡: {{ member.handicap ?? '-' }} / 에버리지: {{ member.average ?? '-' }}</div>
|
||||
<div class="member-meta">{{ member.comment }}</div>
|
||||
@@ -55,8 +46,16 @@
|
||||
@blur="onScoreBlur(member, n, teamNumber)"
|
||||
@keydown.enter="onScoreEnter(member, n, teamNumber)"
|
||||
/>
|
||||
<span v-else>{{ member.scores?.[n - 1] ?? '-' }}</span>
|
||||
|
||||
<span
|
||||
v-else-if="
|
||||
!canInputScore &&
|
||||
member.scores?.[n-1] &&
|
||||
Number(member.scores[n-1]?.score) !==
|
||||
Number(member.scores[n-1]?.score) + Number(member.scores[n-1]?.handicap ?? member.handicap ?? 0)
|
||||
"
|
||||
>
|
||||
{{ member.scores[n-1]?.score }}
|
||||
</span>
|
||||
<!-- 핸디캡 포함 점수 표시 -->
|
||||
<span :class="scoreColorClass(scoreStates[member.participantId + '-' + n], scoresProxy[member.participantId + '-' + n], member.handicap)"
|
||||
class="score-hc-value score-status-text"
|
||||
@@ -92,6 +91,8 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, watch, ref } from 'vue';
|
||||
import { calculateIndividualRankings, getRankColorClass } from '@/utils/rankingUtils';
|
||||
import { getGenderLabel, getParticipantStatusLabel, getPaymentStatusLabel } from '@/utils/enumMappings';
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
members: Array,
|
||||
|
||||
@@ -132,7 +132,7 @@ const routes = [
|
||||
component: ClubSubscriptionManagement,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin', '모임장', '운영진']
|
||||
requiredRoles: ['admin', 'owner', 'manager']
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -141,7 +141,7 @@ const routes = [
|
||||
component: ClubSettings,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
requiredRoles: ['admin', '모임장', '운영진']
|
||||
requiredRoles: ['admin', 'owner', 'manager']
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -213,7 +213,7 @@ router.beforeEach(async (to, from, next) => {
|
||||
|
||||
// 클럽 구독 관리 또는 설정 페이지에 접근하려는 경우, 권한 확인
|
||||
if ((to.name === 'ClubSubscription' || to.name === 'ClubSettings') &&
|
||||
(userClubRole !== 'admin' && userClubRole !== '모임장' && userClubRole !== '운영진')) {
|
||||
(userClubRole !== 'admin' && userClubRole !== 'owner' && userClubRole !== 'manager')) {
|
||||
next({ name: 'ClubDashboard' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
// enumMappings.js: 영어 enum 값을 한글 라벨로 변환하는 매핑 및 함수 모음
|
||||
|
||||
export const GENDER_OPTIONS = [
|
||||
{ name: '남성', value: 'male' },
|
||||
{ name: '여성', value: 'female' }
|
||||
];
|
||||
export const GENDER_LABELS = GENDER_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.name }),
|
||||
{}
|
||||
);
|
||||
|
||||
export const MEMBER_TYPE_OPTIONS = [
|
||||
{ name: '정회원', value: 'regular' },
|
||||
{ name: '준회원', value: 'associate' },
|
||||
{ name: '게스트', value: 'guest' },
|
||||
{ name: '운영진', value: 'manager' },
|
||||
{ name: '모임장', value: 'owner' }
|
||||
];
|
||||
export const MEMBER_TYPE_LABELS = MEMBER_TYPE_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.name }),
|
||||
{}
|
||||
);
|
||||
|
||||
export const STATUS_OPTIONS = [
|
||||
{ name: '활성', value: 'active' },
|
||||
{ name: '비활성', value: 'inactive' },
|
||||
{ name: '정지', value: 'suspended' },
|
||||
{ name: '삭제', value: 'deleted' }
|
||||
];
|
||||
export const STATUS_LABELS = STATUS_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.name }),
|
||||
{}
|
||||
);
|
||||
|
||||
export const PARTICIPANT_STATUS_OPTIONS = [
|
||||
{ name: '참가예정', value: 'pending' },
|
||||
{ name: '참가확정', value: 'confirmed' },
|
||||
{ name: '취소', value: 'canceled' }
|
||||
];
|
||||
export const PARTICIPANT_STATUS_LABELS = PARTICIPANT_STATUS_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.name }),
|
||||
{}
|
||||
);
|
||||
|
||||
export const EVENT_STATUS_OPTIONS = [
|
||||
{ name: '준비', value: 'ready' },
|
||||
{ name: '활성', value: 'active' },
|
||||
{ name: '완료', value: 'completed' },
|
||||
{ name: '취소', value: 'canceled' },
|
||||
{ name: '삭제', value: 'deleted' }
|
||||
];
|
||||
export const EVENT_STATUS_LABELS = EVENT_STATUS_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.name }),
|
||||
{}
|
||||
);
|
||||
|
||||
export const EVENT_TYPE_OPTIONS = [
|
||||
{ name: '정기전', value: 'regular' },
|
||||
{ name: '교류전', value: 'exchange' },
|
||||
{ name: '토너먼트', value: 'tournament' },
|
||||
{ name: '번개', value: 'lightning' },
|
||||
{ name: '친선전', value: 'friendly' },
|
||||
{ name: '기타', value: 'other' }
|
||||
];
|
||||
export const EVENT_TYPE_LABELS = EVENT_TYPE_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.name }),
|
||||
{}
|
||||
);
|
||||
|
||||
export const PAYMENT_STATUS_OPTIONS = [
|
||||
{ name: '미납', value: 'unpaid' },
|
||||
{ name: '납부완료', value: 'paid' },
|
||||
{ name: '대기', value: 'pending' },
|
||||
{ name: '취소', value: 'canceled' }
|
||||
];
|
||||
export const PAYMENT_STATUS_LABELS = PAYMENT_STATUS_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.name }),
|
||||
{}
|
||||
);
|
||||
|
||||
|
||||
export const AVERAGE_CALCULATION_PERIOD_OPTIONS = [
|
||||
{ name: '전체', value: 'total' },
|
||||
{ name: '최근 1년', value: '1year' },
|
||||
{ name: '최근 6개월', value: '6month' },
|
||||
{ name: '최근 3개월', value: '3month' },
|
||||
{ name: '상반기', value: 'firsthalf' },
|
||||
{ name: '하반기', value: 'secondhalf' },
|
||||
{ name: '분기별', value: 'quarterly' }
|
||||
];
|
||||
export const AVERAGE_CALCULATION_PERIOD_LABELS = AVERAGE_CALCULATION_PERIOD_OPTIONS.reduce(
|
||||
(acc, cur) => ({ ...acc, [cur.value]: cur.name }),
|
||||
{}
|
||||
);
|
||||
|
||||
export function getGenderLabel(code) {
|
||||
return GENDER_LABELS[code] || '-';
|
||||
}
|
||||
export function getMemberTypeLabel(code) {
|
||||
return MEMBER_TYPE_LABELS[code] || '-';
|
||||
}
|
||||
export function getStatusLabel(code) {
|
||||
return STATUS_LABELS[code] || '-';
|
||||
}
|
||||
|
||||
export function getParticipantStatusLabel(code) {
|
||||
return PARTICIPANT_STATUS_LABELS[code] || '-';
|
||||
}
|
||||
|
||||
export function getEventStatusLabel(code) {
|
||||
return EVENT_STATUS_LABELS[code] || '-';
|
||||
}
|
||||
export function getEventTypeLabel(code) {
|
||||
return EVENT_TYPE_LABELS[code] || '-';
|
||||
}
|
||||
|
||||
export function getPaymentStatusLabel(code) {
|
||||
return PAYMENT_STATUS_LABELS[code] || '-';
|
||||
}
|
||||
|
||||
export function getAverageCalculationPeriodLabel(code) {
|
||||
return AVERAGE_CALCULATION_PERIOD_LABELS[code] || '-';
|
||||
}
|
||||
|
||||
export function getMemberTypeSeverity(type) {
|
||||
switch (type) {
|
||||
case 'regular': return 'success';
|
||||
case 'associate': return 'info';
|
||||
case 'guest': return 'warning';
|
||||
case 'manager': return 'primary';
|
||||
case 'owner': return 'danger';
|
||||
default: return 'secondary';
|
||||
}
|
||||
}
|
||||
|
||||
// 이벤트 유형별 Badge 색상 반환
|
||||
export function getEventTypeSeverity(type) {
|
||||
switch (type) {
|
||||
case 'regular': return 'success';
|
||||
case 'exchange': return 'primary';
|
||||
case 'tournament': return 'warning';
|
||||
case 'lightning': return 'info';
|
||||
case 'friendly': return 'success';
|
||||
case 'other': return 'secondary';
|
||||
default: return 'secondary';
|
||||
}
|
||||
}
|
||||
|
||||
export function getStatusSeverity(status) {
|
||||
switch (status) {
|
||||
case 'ready': return 'info';
|
||||
case 'active': return 'success';
|
||||
case 'completed': return 'primary';
|
||||
case 'canceled': return 'danger';
|
||||
case 'deleted': return 'secondary';
|
||||
default: return 'secondary';
|
||||
}
|
||||
}
|
||||
|
||||
export function getPaymentStatusSeverity(status) {
|
||||
switch (status) {
|
||||
case 'unpaid': return 'danger';
|
||||
case 'paid': return 'success';
|
||||
case 'pending': return 'warning';
|
||||
case 'canceled': return 'danger';
|
||||
default: return 'secondary';
|
||||
}
|
||||
}
|
||||
|
||||
export function getParticipantStatusSeverity(status) {
|
||||
switch (status) {
|
||||
case 'pending': return 'info';
|
||||
case 'confirmed': return 'success';
|
||||
case 'canceled': return 'danger';
|
||||
default: return 'secondary';
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export const getEventTypeName = (eventType) => {
|
||||
switch (eventType) {
|
||||
case 'meeting':
|
||||
return '정기 모임';
|
||||
case 'tournament':
|
||||
case 'regular':
|
||||
return '토너먼트';
|
||||
case 'training':
|
||||
return '교육/강습';
|
||||
@@ -31,7 +31,7 @@ export const getEventTypeSeverity = (eventType) => {
|
||||
switch (eventType) {
|
||||
case 'meeting':
|
||||
return 'info';
|
||||
case 'tournament':
|
||||
case 'regular':
|
||||
return 'danger';
|
||||
case 'training':
|
||||
return 'success';
|
||||
@@ -57,7 +57,7 @@ export const getStatusName = (status) => {
|
||||
return '진행중';
|
||||
case 'completed':
|
||||
return '완료됨';
|
||||
case 'cancelled':
|
||||
case 'canceled':
|
||||
return '취소됨';
|
||||
default:
|
||||
return status;
|
||||
@@ -77,7 +77,7 @@ export const getStatusSeverity = (status) => {
|
||||
return 'warning';
|
||||
case 'completed':
|
||||
return 'success';
|
||||
case 'cancelled':
|
||||
case 'canceled':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'info';
|
||||
@@ -97,22 +97,24 @@ export const formatDate = (date) => {
|
||||
/**
|
||||
* 이벤트 유형 옵션
|
||||
*/
|
||||
import { getEventTypeLabel, getEventStatusLabel } from '@/utils/enumMappings';
|
||||
|
||||
export const eventTypeOptions = [
|
||||
{ name: '정기 모임', value: 'meeting' },
|
||||
{ name: '토너먼트', value: 'tournament' },
|
||||
{ name: '교육/강습', value: 'training' },
|
||||
{ name: '친목 모임', value: 'social' },
|
||||
{ name: '기타', value: 'other' }
|
||||
{ name: getEventTypeLabel('meeting'), value: 'meeting' },
|
||||
{ name: getEventTypeLabel('regular'), value: 'regular' },
|
||||
{ name: getEventTypeLabel('training'), value: 'training' },
|
||||
{ name: getEventTypeLabel('social'), value: 'social' },
|
||||
{ name: getEventTypeLabel('other'), value: 'other' }
|
||||
];
|
||||
|
||||
/**
|
||||
* 상태 옵션
|
||||
*/
|
||||
export const statusOptions = [
|
||||
{ name: '예정됨', value: 'scheduled' },
|
||||
{ name: '진행중', value: 'in_progress' },
|
||||
{ name: '완료됨', value: 'completed' },
|
||||
{ name: '취소됨', value: 'cancelled' }
|
||||
{ name: getEventStatusLabel('scheduled'), value: 'scheduled' },
|
||||
{ name: getEventStatusLabel('in_progress'), value: 'in_progress' },
|
||||
{ name: getEventStatusLabel('completed'), value: 'completed' },
|
||||
{ name: getEventStatusLabel('canceled'), value: 'canceled' }
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</Column>
|
||||
<Column field="owner.name" header="모임장" sortable style="width: 15%">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.owner?.name || '-' }}
|
||||
{{ slotProps.data.owner?.name || 'guest' }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="memberCount" header="회원 수" sortable style="width: 10%"></Column>
|
||||
@@ -125,13 +125,16 @@
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="gender" header="성별" sortable style="min-width: 6rem"></Column>
|
||||
<Column field="gender" header="성별" sortable style="min-width: 6rem">
|
||||
<template #body="slotProps">
|
||||
{{ getGenderLabel(slotProps.data.gender) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="memberType" header="회원 유형" sortable style="min-width: 8rem">
|
||||
<template #body="slotProps">
|
||||
<Select v-model="slotProps.data.memberType"
|
||||
:options="memberTypes"
|
||||
@change="updateMemberType(slotProps.data)"
|
||||
:disabled="slotProps.data.memberType === '모임장' && !canChangeOwner" />
|
||||
<span>
|
||||
{{ getMemberTypeLabel(slotProps.data.memberType) }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="handicap" header="핸디캡" sortable style="min-width: 6rem"></Column>
|
||||
@@ -154,7 +157,7 @@
|
||||
<Button icon="pi pi-trash"
|
||||
class="p-button-rounded p-button-danger"
|
||||
@click="confirmDeleteMember(slotProps.data)"
|
||||
:disabled="slotProps.data.memberType === '모임장'" />
|
||||
:disabled="slotProps.data.memberType === 'owner'" />
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
@@ -172,16 +175,22 @@
|
||||
</div>
|
||||
<div class="field mb-3">
|
||||
<label for="gender">성별</label>
|
||||
<Select id="gender" v-model="member.gender"
|
||||
:options="['남성', '여성']" placeholder="성별 선택"
|
||||
:class="{'p-invalid': memberSubmitted && !member.gender}" class="w-full" />
|
||||
<Select id="gender" v-model="member.gender"
|
||||
:options="[{ label: '남성', value: 'male' }, { label: '여성', value: 'female' }]" optionLabel="label" optionValue="value" placeholder="성별 선택"
|
||||
:class="{'p-invalid': memberSubmitted && !member.gender}" class="w-full" />
|
||||
<small class="p-error" v-if="memberSubmitted && !member.gender">성별은 필수입니다.</small>
|
||||
</div>
|
||||
<div class="field mb-3">
|
||||
<label for="memberType">회원 유형</label>
|
||||
<Select id="memberType" v-model="member.memberType"
|
||||
:options="memberTypes" placeholder="회원 유형 선택"
|
||||
:class="{'p-invalid': memberSubmitted && !member.memberType}" class="w-full" />
|
||||
<Select id="memberType" v-model="member.memberType"
|
||||
:options="[
|
||||
{ label: '정회원', value: 'regular' },
|
||||
{ label: '준회원', value: 'associate' },
|
||||
{ label: '게스트', value: 'guest' },
|
||||
{ label: '운영진', value: 'manager' },
|
||||
{ label: '모임장', value: 'owner' }
|
||||
]" optionLabel="label" optionValue="value" placeholder="회원 유형 선택"
|
||||
:class="{'p-invalid': memberSubmitted && !member.memberType}" class="w-full" />
|
||||
<small class="p-error" v-if="memberSubmitted && !member.memberType">회원 유형은 필수입니다.</small>
|
||||
</div>
|
||||
<div class="field mb-3">
|
||||
@@ -234,6 +243,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getGenderLabel, getMemberTypeLabel } from '@/utils/enumMappings';
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -257,7 +267,7 @@ const clubs = ref([]);
|
||||
const clubMembers = ref([]);
|
||||
|
||||
// 회원 유형 목록
|
||||
const memberTypes = ref(['모임장', '정회원', '준회원', '운영진']);
|
||||
const memberTypes = ref(['owner', 'regular', 'associate', 'manager']);
|
||||
|
||||
// 기능 목록
|
||||
const allFeatures = ref([
|
||||
@@ -585,7 +595,7 @@ const openNewMemberDialog = () => {
|
||||
id: null,
|
||||
name: '',
|
||||
gender: null,
|
||||
memberType: '준회원',
|
||||
memberType: 'associate',
|
||||
handicap: 0,
|
||||
phone: '',
|
||||
email: ''
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</Column>
|
||||
<Column field="status" header="상태" sortable style="width: 10%">
|
||||
<template #body="slotProps">
|
||||
<Badge :value="getStatusName(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||
<Badge :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="작업" style="width: 10%">
|
||||
@@ -198,6 +198,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getStatusLabel } from '@/utils/enumMappings';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import axios from 'axios';
|
||||
@@ -498,7 +499,7 @@ const deleteSubscription = async () => {
|
||||
};
|
||||
|
||||
// 상태 이름 가져오기
|
||||
const getStatusName = (status) => {
|
||||
// getStatusName 함수 제거(일원화)
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return '활성';
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</Column>
|
||||
<Column field="status" header="상태" sortable style="width: 10%">
|
||||
<template #body="slotProps">
|
||||
<Badge :value="getStatusName(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||
<Badge :value="getStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<Select v-model="filterModel.value" @change="filterCallback()" :options="statusOptions" optionLabel="name" optionValue="value" placeholder="상태 선택" class="p-column-filter" />
|
||||
@@ -123,6 +123,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getStatusLabel } from '@/utils/enumMappings';
|
||||
import { ref, onMounted, reactive, computed } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import adminService from '@/services/adminService';
|
||||
@@ -155,6 +156,11 @@ const roleOptions = [
|
||||
|
||||
// 상태 옵션
|
||||
const statusOptions = [
|
||||
{ name: getStatusLabel('active'), value: 'active' },
|
||||
{ name: getStatusLabel('inactive'), value: 'inactive' },
|
||||
{ name: getStatusLabel('pending'), value: 'pending' },
|
||||
{ name: getStatusLabel('suspended'), value: 'suspended' }
|
||||
];
|
||||
{ name: '활성', value: 'active' },
|
||||
{ name: '비활성', value: 'inactive' },
|
||||
{ name: '정지', value: 'suspended' }
|
||||
@@ -296,7 +302,7 @@ const getRoleSeverity = (role) => {
|
||||
};
|
||||
|
||||
// 상태 이름 가져오기
|
||||
const getStatusName = (status) => {
|
||||
// getStatusName 함수 제거(일원화)
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return '활성';
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
<small v-if="clubDescriptionError" class="p-error">{{ clubDescriptionError }}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="averageCalculationPeriod">평균 산정 기준</label>
|
||||
<select id="averageCalculationPeriod" v-model="averageCalculationPeriod" class="w-full">
|
||||
<option value="all">전체</option>
|
||||
<option value="1year">최근 1년</option>
|
||||
<option value="6months">최근 6개월</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="clubLocation">활동 볼링장</label>
|
||||
<InputText id="clubLocation" v-model="clubLocation" class="w-full" />
|
||||
@@ -45,6 +53,7 @@ const clubName = ref('');
|
||||
const clubDescription = ref('');
|
||||
const clubLocation = ref('');
|
||||
const clubContact = ref('');
|
||||
const averageCalculationPeriod = ref('all');
|
||||
const loading = ref(false);
|
||||
const clubNameError = ref('');
|
||||
const clubDescriptionError = ref('');
|
||||
@@ -92,6 +101,7 @@ const handleCreateClub = async () => {
|
||||
description: clubDescription.value,
|
||||
location: clubLocation.value,
|
||||
contact: clubContact.value,
|
||||
averageCalculationPeriod: averageCalculationPeriod.value,
|
||||
ownerEmail: authStore.user.email
|
||||
});
|
||||
|
||||
|
||||
@@ -10,31 +10,42 @@
|
||||
<div class="settings-section">
|
||||
<h2>기본 정보</h2>
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="clubName">클럽명</label>
|
||||
<input type="text" id="clubName" v-model="clubInfo.name" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="clubDescription">클럽 설명</label>
|
||||
<textarea id="clubDescription" v-model="clubInfo.description" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="clubLocation">위치</label>
|
||||
<input type="text" id="clubLocation" v-model="clubInfo.location" class="form-control" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="femaleHandicap">여성 기본핸디 설정</label>
|
||||
<input type="number" id="femaleHandicap" v-model="clubInfo.femaleHandicap" class="form-control" min="0" max="100" />
|
||||
<small class="form-text text-muted">여성 회원을 멤버(게스트)로 등록할 때 기본 핸디캡 값입니다.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="clubOwner">모임장 설정</label>
|
||||
<select id="clubOwner" v-model="selectedOwnerId" class="form-control">
|
||||
<option v-for="member in members" :key="member.id" :value="member.userId">
|
||||
{{ member.name }} ({{ member.memberType }}) [ID: {{ member.userId }}]
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">모임장 권한을 이전할 회원을 선택합니다. 모임장은 클럽의 모든 관리 권한을 가집니다.</small>
|
||||
<div class="grid">
|
||||
<div class="field col-12">
|
||||
<label for="clubName">클럽명</label>
|
||||
<input type="text" id="clubName" v-model="clubInfo.name" class="form-control" />
|
||||
</div>
|
||||
<div class="field col-12">
|
||||
<label for="clubDescription">클럽 설명</label>
|
||||
<textarea id="clubDescription" v-model="clubInfo.description" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="field col-4">
|
||||
<label for="clubLocation">위치</label>
|
||||
<input type="text" id="clubLocation" v-model="clubInfo.location" class="form-control" />
|
||||
</div>
|
||||
<div class="field col-4">
|
||||
<label for="femaleHandicap">여성 기본핸디 설정</label>
|
||||
<input type="number" id="femaleHandicap" v-model="clubInfo.femaleHandicap" class="form-control" min="0" max="100" />
|
||||
<small class="form-text text-muted">여성 회원을 멤버(게스트)로 등록할 때 기본 핸디캡 값입니다.</small>
|
||||
</div>
|
||||
<div class="field col-4">
|
||||
<label for="averageCalculationPeriod">평균 산정 기준</label>
|
||||
<select id="averageCalculationPeriod" v-model="clubInfo.averageCalculationPeriod" class="form-control">
|
||||
<option v-for="option in averageCalculationPeriodOptions" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">클럽의 에버리지(평균) 산정 기준을 선택하세요.</small>
|
||||
</div>
|
||||
<div class="field col-12">
|
||||
<label for="clubOwner">모임장 설정</label>
|
||||
<select id="clubOwner" v-model="selectedOwnerId" class="form-control">
|
||||
<option v-for="member in members" :key="member.id" :value="member.userId">
|
||||
{{ member.name }} ({{ member.memberType }}) [ID: {{ member.userId }}]
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">모임장 권한을 이전할 회원을 선택합니다. 모임장은 클럽의 모든 관리 권한을 가집니다.</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button @click="saveClubInfo" class="btn btn-primary">저장</button>
|
||||
@@ -120,6 +131,17 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getAverageCalculationPeriodLabel } from '@/utils/enumMappings';
|
||||
|
||||
const averageCalculationPeriodOptions = [
|
||||
{ value: 'total', label: getAverageCalculationPeriodLabel('total') },
|
||||
{ value: '1year', label: getAverageCalculationPeriodLabel('1year') },
|
||||
{ value: '6month', label: getAverageCalculationPeriodLabel('6month') },
|
||||
{ value: '3month', label: getAverageCalculationPeriodLabel('3month') },
|
||||
{ value: 'firsthalf', label: getAverageCalculationPeriodLabel('firsthalf') },
|
||||
{ value: 'secondhalf', label: getAverageCalculationPeriodLabel('secondhalf') },
|
||||
{ value: 'quarterly', label: getAverageCalculationPeriodLabel('quarterly') }
|
||||
];
|
||||
import { ref, onMounted, computed, inject } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
@@ -139,7 +161,8 @@ const clubInfo = ref({
|
||||
description: '',
|
||||
location: '',
|
||||
femaleHandicap: 15, // 기본값 설정
|
||||
ownerId: null // 모임장 ID
|
||||
ownerId: null, // 모임장 ID
|
||||
averageCalculationPeriod: 'all' // 평균 산정 기준 기본값
|
||||
});
|
||||
|
||||
// 구독 정보
|
||||
@@ -208,7 +231,8 @@ const loadClubInfo = async () => {
|
||||
location: clubData.location || '',
|
||||
memberCount: clubData.memberCount || 0,
|
||||
femaleHandicap: clubData.femaleHandicap || 15,
|
||||
ownerId: clubData.ownerId // 모임장 ID 추가
|
||||
ownerId: clubData.ownerId, // 모임장 ID 추가
|
||||
averageCalculationPeriod: clubData.averageCalculationPeriod || '6month' // 평균 산정 기준
|
||||
};
|
||||
|
||||
// 선택된 모임장 ID 설정
|
||||
@@ -311,7 +335,8 @@ const saveClubInfo = async () => {
|
||||
name: clubInfo.value.name,
|
||||
description: clubInfo.value.description,
|
||||
location: clubInfo.value.location,
|
||||
femaleHandicap: clubInfo.value.femaleHandicap
|
||||
femaleHandicap: clubInfo.value.femaleHandicap,
|
||||
averageCalculationPeriod: clubInfo.value.averageCalculationPeriod
|
||||
};
|
||||
|
||||
// 모임장이 변경되었다면 확인 메시지 표시
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
<EventDetailsDialog
|
||||
v-model:visible="eventDetailsDialog"
|
||||
:event="selectedEvent"
|
||||
:getEventTypeName="getEventTypeName"
|
||||
:getEventTypeLabel="getEventTypeLabel"
|
||||
:getEventTypeSeverity="getEventTypeSeverity"
|
||||
:getStatusName="getStatusName"
|
||||
:getEventStatusLabel="getEventStatusLabel"
|
||||
:getStatusSeverity="getStatusSeverity"
|
||||
:formatDate="formatDate"
|
||||
@edit="navigateToEventManagement"
|
||||
@@ -27,6 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// getEventTypeLabel 중복 import 제거. eventUtils.js의 getEventTypeLabel만 사용.
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
@@ -37,12 +38,12 @@ import koLocale from '@fullcalendar/core/locales/ko';
|
||||
import EventDetailsDialog from '@/components/event/EventDetailsDialog.vue';
|
||||
import eventService from '@/services/eventService';
|
||||
import {
|
||||
getEventTypeName,
|
||||
getEventTypeSeverity,
|
||||
getStatusName,
|
||||
getStatusSeverity,
|
||||
formatDate
|
||||
} from '@/utils/eventUtils';
|
||||
import { getEventTypeLabel, getEventStatusLabel } from '@/utils/enumMappings';
|
||||
// getEventTypeLabel, getEventStatusLabel은 enumMappings.js에서만 import, eventUtils.js에서는 import/export하지 않음
|
||||
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
@@ -88,17 +89,17 @@ const fetchEvents = async () => {
|
||||
// 이벤트 타입에 따른 색상 지정
|
||||
const getEventColor = (eventType) => {
|
||||
switch (eventType) {
|
||||
case '정기전':
|
||||
case 'regular':
|
||||
return '#FF5722'; // 주황색
|
||||
case '번개':
|
||||
case 'lightning':
|
||||
return '#2196F3'; // 파란색
|
||||
case '연습':
|
||||
case 'practice':
|
||||
return '#4CAF50'; // 초록색
|
||||
case '교류전':
|
||||
case 'exchange':
|
||||
return '#9C27B0'; // 보라색
|
||||
case '이벤트':
|
||||
case 'event':
|
||||
return '#FFC107'; // 노란색
|
||||
case '기타':
|
||||
case 'other':
|
||||
return '#607D8B'; // 회색
|
||||
default:
|
||||
return '#3f51b5'; // 기본색
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
>
|
||||
<Column field="eventType" header="유형" sortable style="width: 10%">
|
||||
<template #body="slotProps">
|
||||
<Badge :value="getEventTypeName(slotProps.data.eventType)" :severity="getEventTypeSeverity(slotProps.data.eventType)" />
|
||||
<Badge :value="getEventTypeLabel(slotProps.data.eventType)" :severity="getEventTypeSeverity(slotProps.data.eventType)" />
|
||||
</template>
|
||||
<template #filter="{ filterModel, filterCallback }">
|
||||
<Select v-model="filterModel.value" @change="filterCallback()" :options="eventTypeOptions" optionLabel="name" optionValue="value" placeholder="유형 선택" class="p-column-filter" />
|
||||
@@ -52,7 +52,7 @@
|
||||
<Column field="maxParticipants" header="최대 인원" sortable style="width: 8%"></Column>
|
||||
<Column field="status" header="상태" sortable style="width: 8%">
|
||||
<template #body="slotProps">
|
||||
<Badge :value="slotProps.data.status" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||
<Badge :value="getEventStatusLabel(slotProps.data.status)" :severity="getStatusSeverity(slotProps.data.status)" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column header="작업" style="width: 10%">
|
||||
@@ -80,9 +80,9 @@
|
||||
<EventDetailsDialog
|
||||
v-model:visible="detailsDialog"
|
||||
:event="event || {}"
|
||||
:getEventTypeName="getEventTypeName"
|
||||
:getEventTypeLabel="getEventTypeLabel"
|
||||
:getEventTypeSeverity="getEventTypeSeverity"
|
||||
:getStatusName="getStatusName"
|
||||
:getEventStatusLabel="getEventStatusLabel"
|
||||
:getStatusSeverity="getStatusSeverity"
|
||||
:formatDate="formatDate"
|
||||
:availableMembers="clubMembers"
|
||||
@@ -120,21 +120,14 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getEventTypeLabel, getEventStatusLabel, getStatusSeverity } from '@/utils/enumMappings';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useClubStore } from '@/stores/clubStore';
|
||||
import FileUploadDialog from '@/components/event/FileUploadDialog.vue';
|
||||
import eventService from '@/services/eventService';
|
||||
import apiClient from '@/services/api';
|
||||
import dayjs from 'dayjs';
|
||||
import clubService from '@/services/clubService';
|
||||
import ScoreManagement from '@/components/event/ScoreManagement.vue';
|
||||
import ParticipantManagement from '@/components/event/ParticipantManagement.vue';
|
||||
import TabView from 'primevue/tabview';
|
||||
import TabPanel from 'primevue/tabpanel';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Button from 'primevue/button';
|
||||
import EventDialog from '@/components/event/EventDialog.vue';
|
||||
import EventDetailsDialog from '@/components/event/EventDetailsDialog.vue';
|
||||
|
||||
@@ -164,15 +157,6 @@ const filters = ref({
|
||||
// 팀 기능 사용 권한 여부
|
||||
const hasTeamFeature = computed(() => clubStore.hasFeature('team_create'));
|
||||
|
||||
// 이벤트 유형 옵션
|
||||
const eventTypeOptions = [
|
||||
{ name: '정기전', value: '정기전' },
|
||||
{ name: '토너먼트', value: '토너먼트' },
|
||||
{ name: '연습', value: '연습' },
|
||||
{ name: '친선전', value: '친선전' },
|
||||
{ name: '기타', value: '기타' }
|
||||
];
|
||||
|
||||
// 클럽 멤버 목록
|
||||
const clubMembers = ref([]);
|
||||
|
||||
@@ -244,42 +228,10 @@ const formatDate = (date) => {
|
||||
// 이벤트 유형에 따른 Badge 스타일
|
||||
const getEventTypeSeverity = (type) => {
|
||||
switch (type) {
|
||||
case '정기전': return 'success';
|
||||
case '토너먼트': return 'warning';
|
||||
case '연습': return 'info';
|
||||
case '친선전': return 'primary';
|
||||
default: return 'secondary';
|
||||
}
|
||||
};
|
||||
|
||||
// 이벤트 유형 이름 반환
|
||||
const getEventTypeName = (type) => {
|
||||
return type || '기타';
|
||||
};
|
||||
|
||||
// 이벤트 상태 이름 반환
|
||||
const STATUS_LABELS = {
|
||||
'준비': '준비',
|
||||
'진행중': '진행중',
|
||||
'완료': '완료',
|
||||
'취소': '취소',
|
||||
'예정됨': '예정됨',
|
||||
'in_progress': '진행중',
|
||||
'completed': '완료',
|
||||
'cancelled': '취소',
|
||||
'활성': '활성',
|
||||
'비활성': '비활성',
|
||||
'삭제': '삭제',
|
||||
};
|
||||
const getStatusName = (status) => STATUS_LABELS[status] || status;
|
||||
|
||||
// 이벤트 상태에 따른 Badge 스타일
|
||||
const getStatusSeverity = (status) => {
|
||||
switch (status) {
|
||||
case '준비': return 'info';
|
||||
case '진행중': return 'warning';
|
||||
case '완료': return 'success';
|
||||
case '취소': return 'danger';
|
||||
case 'regular': return 'success';
|
||||
case 'tournament': return 'warning';
|
||||
case 'practice': return 'info';
|
||||
case 'friendly': return 'primary';
|
||||
default: return 'secondary';
|
||||
}
|
||||
};
|
||||
@@ -289,7 +241,7 @@ const openNewEventDialog = () => {
|
||||
event.value = {
|
||||
clubId: clubId.value,
|
||||
status: '준비',
|
||||
eventType: '정기전',
|
||||
eventType: 'regular',
|
||||
title: '',
|
||||
description: '',
|
||||
startDate: null,
|
||||
|
||||
@@ -36,11 +36,15 @@
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="gender" header="성별" sortable style="width: 10%"></Column>
|
||||
<Column field="gender" header="성별" sortable style="width: 10%">
|
||||
<template #body="slotProps">
|
||||
{{ getGenderLabel(slotProps.data.gender) }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="memberType" header="회원 유형" sortable style="width: 15%">
|
||||
<template #body="slotProps">
|
||||
<span :class="getMemberTypeClass(slotProps.data.memberType)">
|
||||
{{ slotProps.data.memberType }}
|
||||
<span>
|
||||
{{ getMemberTypeLabel(slotProps.data.memberType) }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
@@ -54,7 +58,7 @@
|
||||
</Column>
|
||||
<Column field="status" header="상태" sortable style="width: 10%">
|
||||
<template #body="slotProps">
|
||||
<span :class="`status-${slotProps.data.status}`">
|
||||
<span>
|
||||
{{ getStatusLabel(slotProps.data.status) }}
|
||||
</span>
|
||||
</template>
|
||||
@@ -81,11 +85,11 @@
|
||||
</div>
|
||||
<div class="field col-6 mb-3">
|
||||
<label for="gender" class="mb-1">성별</label>
|
||||
<Select id="gender" v-model="member.gender" :options="genderOptions" optionLabel="name" optionValue="code" placeholder="성별 선택" class="w-full" />
|
||||
<Select id="gender" v-model="member.gender" :options="GENDER_OPTIONS" optionLabel="name" optionValue="value" placeholder="성별 선택" class="w-full" />
|
||||
</div>
|
||||
<div class="field col-6 mb-3">
|
||||
<label for="memberType" class="mb-1">회원 유형</label>
|
||||
<Select id="memberType" v-model="member.memberType" :options="memberTypeOptions" optionLabel="name" optionValue="code" placeholder="회원 유형 선택" class="w-full" />
|
||||
<Select id="memberType" v-model="member.memberType" :options="filteredMemberTypeOptions" optionLabel="name" optionValue="code" placeholder="회원 유형 선택" class="w-full" />
|
||||
</div>
|
||||
<div class="field col-6 mb-3">
|
||||
<label for="phone" class="mb-1">전화번호</label>
|
||||
@@ -101,7 +105,7 @@
|
||||
</div>
|
||||
<div class="field col-6 mb-3">
|
||||
<label for="status" class="mb-1">상태</label>
|
||||
<Select id="status" v-model="member.status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="상태 선택" class="w-full" />
|
||||
<Select id="status" v-model="member.status" :options="STATUS_OPTIONS" optionLabel="name" optionValue="value" placeholder="상태 선택" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-content-end gap-2 mt-4">
|
||||
@@ -129,7 +133,7 @@
|
||||
<div class="col-12 md:col-4">
|
||||
<div class="field">
|
||||
<label>회원 유형</label>
|
||||
<MultiSelect v-model="filters.memberTypes" :options="memberTypes" optionLabel="name"
|
||||
<MultiSelect v-model="filters.memberTypes" :options="filteredMemberTypeOptions" optionLabel="name"
|
||||
placeholder="선택" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,23 +194,21 @@ import { ref, reactive, computed, onMounted, watch } from 'vue';
|
||||
import clubService from '@/services/clubService';
|
||||
import { useClubStore } from '@/stores/clubStore';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import {
|
||||
MEMBER_TYPE_OPTIONS,
|
||||
GENDER_OPTIONS,
|
||||
STATUS_OPTIONS,
|
||||
getGenderLabel,
|
||||
getMemberTypeLabel,
|
||||
getStatusLabel
|
||||
} from '@/utils/enumMappings';
|
||||
// 모임장(owner) 옵션을 제외한 회원 유형 옵션만 노출
|
||||
const filteredMemberTypeOptions = computed(() => MEMBER_TYPE_OPTIONS.filter(opt => opt.value !== 'owner' && opt.code !== 'owner'));
|
||||
|
||||
const clubStore = useClubStore();
|
||||
const toast = useToast();
|
||||
|
||||
// 회원 유형 옵션
|
||||
const memberTypeOptions = ref([
|
||||
{ name: '정회원', code: '정회원' },
|
||||
{ name: '준회원', code: '준회원' },
|
||||
{ name: '운영진', code: '운영진' }
|
||||
// 모임장 옵션은 제거 - 클럽 설정에서만 변경 가능하도록 제한
|
||||
]);
|
||||
|
||||
// 성별 옵션
|
||||
const genderOptions = ref([
|
||||
{ name: '남성', code: '남성' },
|
||||
{ name: '여성', code: '여성' }
|
||||
]);
|
||||
|
||||
// 회원 목록
|
||||
const members = ref([]);
|
||||
@@ -327,16 +329,7 @@ const formatDate = (dateStr) => {
|
||||
return `${date.getFullYear()}.${String(date.getMonth() + 1).padStart(2, '0')}.${String(date.getDate()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 회원 상태 표시 함수
|
||||
const getStatusLabel = (status) => {
|
||||
switch (status) {
|
||||
case 'active': return '활성';
|
||||
case 'inactive': return '비활성';
|
||||
case 'suspended': return '정지';
|
||||
case 'deleted': return '삭제됨';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 컴포넌트 마운트 시 데이터 로드
|
||||
onMounted(async () => {
|
||||
@@ -367,16 +360,7 @@ const loadMembers = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 회원 유형에 따른 클래스 반환
|
||||
const getMemberTypeClass = (type) => {
|
||||
switch (type) {
|
||||
case '모임장': return 'member-type-owner';
|
||||
case '운영진': return 'member-type-admin';
|
||||
case '정회원': return 'member-type-regular';
|
||||
case '준회원': return 'member-type-associate';
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 새 회원 추가 다이얼로그 열기
|
||||
const openNewMemberDialog = () => {
|
||||
@@ -464,11 +448,7 @@ const deleteMember = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const statusOptions = ref([
|
||||
{ label: '활성', value: 'active' },
|
||||
{ label: '비활성', value: 'inactive' },
|
||||
{ label: '정지', value: 'suspended' }
|
||||
]);
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -105,6 +105,76 @@
|
||||
</table>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
@@ -119,10 +189,7 @@ ChartJS.register(CategoryScale, LinearScale, BarElement, LineElement, PointEleme
|
||||
|
||||
export default {
|
||||
name: 'Statistics',
|
||||
components: {
|
||||
BarChart,
|
||||
LineChart
|
||||
},
|
||||
components: { BarChart, LineChart },
|
||||
setup() {
|
||||
const stats = ref({
|
||||
summary: {},
|
||||
@@ -144,9 +211,11 @@ export default {
|
||||
});
|
||||
|
||||
stats.value = response.data;
|
||||
console.log('[통계] API 응답:', response.data);
|
||||
} catch (error) {
|
||||
console.error('통계 데이터 로드 오류:', error);
|
||||
} finally {
|
||||
console.log('[통계] 최종 stats 상태:', stats.value);
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
@@ -194,18 +263,39 @@ export default {
|
||||
const attendanceTrendChartOptions = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
legend: { display: false }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: 100
|
||||
}
|
||||
}
|
||||
scales: { y: { beginAtZero: true, max: 100 } }
|
||||
};
|
||||
|
||||
// 추가 통계용 차트 데이터 (순서 주의!)
|
||||
const participantTrendChartData = computed(() => {
|
||||
const arr = stats.value.participantTrend || [];
|
||||
return {
|
||||
labels: arr.map(x => formatDate(x.date)),
|
||||
datasets: [{
|
||||
label: '참가자 수',
|
||||
data: arr.map(x => x.participantCount),
|
||||
backgroundColor: '#42b983',
|
||||
borderColor: '#42b983',
|
||||
fill: false
|
||||
}]
|
||||
};
|
||||
});
|
||||
|
||||
function getHandicapTrendChartData(trendArr) {
|
||||
if (!trendArr) return null;
|
||||
return {
|
||||
labels: trendArr.map(t => formatDate(t.date)),
|
||||
datasets: [{
|
||||
label: '평균 핸디캡',
|
||||
data: trendArr.map(t => t.avgHandicap),
|
||||
borderColor: '#f87979',
|
||||
fill: false
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStatistics();
|
||||
});
|
||||
@@ -219,7 +309,9 @@ export default {
|
||||
scoreDistributionChartData,
|
||||
scoreDistributionChartOptions,
|
||||
attendanceTrendChartData,
|
||||
attendanceTrendChartOptions
|
||||
attendanceTrendChartOptions,
|
||||
participantTrendChartData,
|
||||
getHandicapTrendChartData
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,10 +25,8 @@
|
||||
<template v-if="event">
|
||||
<Divider class="mb-4" />
|
||||
<div class="event-tag mb-2">
|
||||
<Tag :value="event.eventType" class="event-type-tag" />
|
||||
<Tag :value="event.status"
|
||||
:severity="event.status === '완료' ? 'success' : event.status === '활성' ? 'info' : 'warning'"
|
||||
class="ml-1" />
|
||||
<Tag :value="getEventTypeLabel(event.eventType)" :severity="getEventTypeSeverity(event.eventType)" />
|
||||
<Tag :value="getEventStatusLabel(event.status)" :severity="getStatusSeverity(event.status)" class="ml-1" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-content-end gap-2">
|
||||
@@ -99,7 +97,7 @@
|
||||
</DataTable>
|
||||
</template>
|
||||
</Card>
|
||||
<template v-if="event.status != '완료'">
|
||||
<template v-if="event.status != 'completed'">
|
||||
<div v-for="team in getAllTeamsWithUnassigned()" :key="team.id" class="mb-4">
|
||||
<Card>
|
||||
<template #content>
|
||||
@@ -154,7 +152,7 @@
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="event.status != '완료'">
|
||||
<template v-if="event.status != 'completed'">
|
||||
<Card>
|
||||
<template #content>
|
||||
<MemberListCard
|
||||
@@ -176,7 +174,7 @@
|
||||
</Card>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="event.status == '완료'">
|
||||
<template v-if="event.status == 'completed'">
|
||||
<Card>
|
||||
<template #title>
|
||||
<div class="card-title-with-button">
|
||||
@@ -244,6 +242,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { getGenderLabel, getMemberTypeLabel, getStatusLabel, getPaymentStatusLabel, getEventStatusLabel, getEventTypeLabel, getStatusSeverity, getEventTypeSeverity } from '@/utils/enumMappings';
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
||||
import {
|
||||
calculateIndividualRankings,
|
||||
@@ -279,9 +278,9 @@ const showJoinDialog = ref(false);
|
||||
const allMembers = ref([]); // 전체 클럽 멤버 목록
|
||||
const availableMembers = ref([]); // 참가 신청 가능 명단
|
||||
// ===== 이벤트 상태별 권한 제어 =====
|
||||
const isReady = computed(() => event.value.status === '준비');
|
||||
const isActive = computed(() => event.value.status === '활성');
|
||||
const isFinished = computed(() => event.value.status === '완료');
|
||||
const isReady = computed(() => event.value.status === 'ready');
|
||||
const isActive = computed(() => event.value.status === 'active');
|
||||
const isFinished = computed(() => event.value.status === 'completed');
|
||||
|
||||
// 신청 마감일시 체크
|
||||
const isRegistrationClosed = computed(() => {
|
||||
@@ -730,17 +729,6 @@ function verifyPassword() {
|
||||
});
|
||||
}
|
||||
|
||||
async function updateScore(team, member) {
|
||||
if (!canEdit.value) return;
|
||||
if (!isAuthenticated.value) return;
|
||||
try {
|
||||
await PublicEventService.updateScore(publicHash, member.id, member.score);
|
||||
reload();
|
||||
} catch (e) {
|
||||
// 에러 핸들링
|
||||
}
|
||||
}
|
||||
|
||||
function reload() {
|
||||
fetchEventData();
|
||||
}
|
||||
@@ -760,17 +748,6 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// (핸디캡 제외) 팀별 게임별 실제 점수 합계
|
||||
function getTeamRawScore(team, gameIdx) {
|
||||
let total = 0;
|
||||
if (!team || !team.members) return 0;
|
||||
team.members.forEach(member => {
|
||||
const score = Number(scoresProxy.value[member.participantId + '-' + gameIdx]) || 0;
|
||||
total += score;
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
// 팀별 게임별 합산 점수 계산 (computed)
|
||||
// 팀별 게임별 합산 점수 계산 함수 (단순 값 반환)
|
||||
function getAllTeamsWithUnassigned() {
|
||||
|
||||
Reference in New Issue
Block a user