시드 생성
This commit is contained in:
+15
-1
@@ -105,7 +105,7 @@ const singleMenuItems = ref([]);
|
||||
const expandedGroups = ref({});
|
||||
const isMobileView = ref(window.innerWidth < 768);
|
||||
const isSidebarVisible = ref(!isMobileView.value);
|
||||
const isLoggedIn = ref(!!localStorage.getItem('token'));
|
||||
const isLoggedIn = computed(() => !!user.value && !!localStorage.getItem('token'));
|
||||
const clubs = ref([]); // 클럽 목록
|
||||
const selectedClubId = ref(localStorage.getItem('clubId') || null); // 선택된 클럽 ID
|
||||
const selectedClub = ref(null);
|
||||
@@ -296,6 +296,16 @@ const loadUserData = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 로그인 성공 후 상태 갱신 핸들러
|
||||
const handleUserLoggedIn = async () => {
|
||||
await loadUserData();
|
||||
if (isLoggedIn.value) {
|
||||
await fetchClubs();
|
||||
await fetchClubInfo();
|
||||
loadMenuItems();
|
||||
}
|
||||
};
|
||||
|
||||
// 컴포넌트 마운트 시 데이터 로드
|
||||
onMounted(async () => {
|
||||
await loadUserData();
|
||||
@@ -308,6 +318,9 @@ onMounted(async () => {
|
||||
// 화면 크기 변경 감지
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// 로그인 성공 이벤트 감지
|
||||
window.addEventListener('user-logged-in', handleUserLoggedIn);
|
||||
|
||||
// 초기 expandedGroups 설정
|
||||
initializeExpandedGroups();
|
||||
});
|
||||
@@ -315,6 +328,7 @@ onMounted(async () => {
|
||||
// 컴포넌트 언마운트 시 이벤트 리스너 제거
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
window.removeEventListener('user-logged-in', handleUserLoggedIn);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ import Avatar from 'primevue/avatar';
|
||||
const router = useRouter();
|
||||
const userMenu = ref(null);
|
||||
const adminMenu = ref(null);
|
||||
const isLoggedIn = computed(() => !!localStorage.getItem('token'));
|
||||
const isLoggedIn = computed(() => !!user.value);
|
||||
const isAdmin = computed(() => {
|
||||
const role = localStorage.getItem('userRole');
|
||||
return role === 'admin' || role === 'superadmin';
|
||||
|
||||
@@ -58,6 +58,52 @@
|
||||
@score-updated="$emit('score-updated')"
|
||||
/>
|
||||
</TabPanel>
|
||||
<!-- 팀 편성 탭 -->
|
||||
<TabPanel header="팀 편성">
|
||||
<div class="team-generation-section">
|
||||
<div class="flex align-items-center gap-2 mb-3">
|
||||
<label>
|
||||
<input type="radio" value="byMemberCount" v-model="teamOption" /> 팀 인원수로 나누기
|
||||
</label>
|
||||
<InputNumber v-if="teamOption==='byMemberCount'" v-model="teamOptionValue" :min="2" :max="eventModel.participants?.length || 2" placeholder="팀 인원수" style="width:120px;" />
|
||||
<label class="ml-4">
|
||||
<input type="radio" value="byTeamCount" v-model="teamOption" /> 팀 개수로 나누기
|
||||
</label>
|
||||
<InputNumber v-if="teamOption==='byTeamCount'" v-model="teamOptionValue" :min="2" :max="eventModel.participants?.length || 2" placeholder="팀 개수" style="width:120px;" />
|
||||
<Button label="팀 생성" icon="pi pi-users" class="ml-3" :disabled="!canGenerateTeams" @click="generateTeams" />
|
||||
</div>
|
||||
<div v-if="teams.length > 0">
|
||||
<div v-for="(team, idx) in teams" :key="idx" class="team-box mb-3 p-3">
|
||||
<div class="flex align-items-center gap-2 mb-2">
|
||||
<strong>팀 {{ idx + 1 }}</strong>
|
||||
<span class="ml-2">(인원: {{ team.members.length }}명)</span>
|
||||
<span class="ml-3">팀 에버리지: <span class="text-primary">{{ getTeamAverage(team).toFixed(1) }}</span></span>
|
||||
<span class="ml-3">핸디캡:</span>
|
||||
<InputNumber v-model="team.handicap" :min="-100" :max="100" style="width:80px;" />
|
||||
</div>
|
||||
<ul class="pl-3">
|
||||
<li v-for="(member, mIdx) in team.members" :key="member.id || mIdx" class="flex align-items-center gap-2 mb-1">
|
||||
<InputText v-model="member.name" style="width: 140px;" placeholder="이름 입력" />
|
||||
<span v-if="getMemberAverage(member) !== null" class="text-xs text-gray-700 ml-1">({{ getMemberAverage(member) }})</span>
|
||||
<Button icon="pi pi-times" class="p-button-text p-button-danger p-0" @click="team.members.splice(mIdx,1)" v-if="team.members.length > 1" />
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex align-items-center gap-2 mt-2">
|
||||
<Dropdown
|
||||
v-model="team._selectedMemberId"
|
||||
:options="availableMembersForTeam(idx)"
|
||||
optionLabel="name"
|
||||
optionValue="id"
|
||||
placeholder="팀원 선택"
|
||||
style="width: 140px;"
|
||||
/>
|
||||
<Button icon="pi pi-plus" class="p-button-text p-0" :disabled="!team._selectedMemberId" @click="addMemberToTeam(idx)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-500 mt-3">팀을 생성하려면 옵션을 선택 후 '팀 생성'을 눌러주세요.</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabView>
|
||||
|
||||
<template #footer>
|
||||
@@ -75,54 +121,142 @@ import TabView from 'primevue/tabview';
|
||||
import TabPanel from 'primevue/tabpanel';
|
||||
import Badge from 'primevue/badge';
|
||||
import Button from 'primevue/button';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import ParticipantManagement from './ParticipantManagement.vue';
|
||||
import ScoreManagement from './ScoreManagement.vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
event: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
getEventTypeName: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getStatusName: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
getStatusSeverity: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
formatDate: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
availableMembers: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
visible: { type: Boolean, required: true },
|
||||
event: { type: Object, default: () => ({}) },
|
||||
getEventTypeName: { type: Function, required: true },
|
||||
getStatusName: { type: Function, required: true },
|
||||
getStatusSeverity: { type: Function, required: true },
|
||||
formatDate: { type: Function, required: true },
|
||||
availableMembers: { type: Array, default: () => [] }
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:visible', 'hide', 'edit', 'participant-updated', 'score-updated']);
|
||||
|
||||
const eventModel = computed(() => props.event);
|
||||
|
||||
const eventModel = computed(() => props.event || {});
|
||||
const formattedStartDate = computed(() => props.formatDate(eventModel.value.startDate));
|
||||
const formattedEndDate = computed(() => props.formatDate(eventModel.value.endDate));
|
||||
|
||||
const hideDialog = () => {
|
||||
emit('hide');
|
||||
};
|
||||
console.log("props", props);
|
||||
console.log("eventModel", eventModel.value);
|
||||
console.log("참가자", JSON.stringify(eventModel.value.participants, null, 2));
|
||||
// 팀 생성 관련 상태
|
||||
const teamOption = ref('byMemberCount');
|
||||
const teamOptionValue = ref(2);
|
||||
const teams = ref([]);
|
||||
|
||||
const canGenerateTeams = computed(() => {
|
||||
const n = eventModel.value.participants?.length || 0;
|
||||
if (n < 2) return false;
|
||||
if (teamOption.value === 'byMemberCount') {
|
||||
return teamOptionValue.value >= 2 && teamOptionValue.value <= n;
|
||||
} else {
|
||||
return teamOptionValue.value >= 2 && teamOptionValue.value <= n;
|
||||
}
|
||||
});
|
||||
|
||||
function shuffle(array) {
|
||||
// Fisher-Yates shuffle
|
||||
let arr = array.slice();
|
||||
for (let i = arr.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function generateTeams() {
|
||||
const participants = (eventModel.value.participants || []).map(p => ({ id: p.id, name: p.name }));
|
||||
const shuffled = shuffle(participants);
|
||||
let result = [];
|
||||
if (teamOption.value === 'byMemberCount') {
|
||||
const size = teamOptionValue.value;
|
||||
const teamCount = Math.ceil(shuffled.length / size);
|
||||
for (let i = 0; i < teamCount; i++) {
|
||||
result.push({ members: [], handicap: 0, _selectedMemberId: null });
|
||||
}
|
||||
shuffled.forEach((member, idx) => {
|
||||
result[Math.floor(idx / size)].members.push(member);
|
||||
});
|
||||
} else {
|
||||
const teamCount = teamOptionValue.value;
|
||||
for (let i = 0; i < teamCount; i++) {
|
||||
result.push({ members: [], handicap: 0, _selectedMemberId: null });
|
||||
}
|
||||
shuffled.forEach((member, idx) => {
|
||||
result[idx % teamCount].members.push(member);
|
||||
});
|
||||
}
|
||||
teams.value = result;
|
||||
}
|
||||
|
||||
// 팀별로 아직 배정되지 않은 참가자만 선택지로 제공
|
||||
function availableMembersForTeam(teamIdx) {
|
||||
const usedIds = new Set();
|
||||
teams.value.forEach((team, idx) => {
|
||||
if (idx === teamIdx) return;
|
||||
team.members.forEach(m => usedIds.add(m.id));
|
||||
});
|
||||
// 팀 내 중복도 막기 위해 현재 팀 내 id도 추가
|
||||
teams.value[teamIdx]?.members.forEach((m, i, arr) => {
|
||||
if (arr.findIndex(x => x.id === m.id) !== i) usedIds.add(m.id);
|
||||
});
|
||||
return (eventModel.value.participants || [])
|
||||
.filter(p => p.id && p.Member && p.Member.name &&
|
||||
!usedIds.has(p.id) &&
|
||||
!teams.value[teamIdx].members.some(m => m.id === p.id)
|
||||
)
|
||||
.map(p => ({ id: p.id, name: p.Member.name }));
|
||||
}
|
||||
|
||||
function addMemberToTeam(teamIdx) {
|
||||
const team = teams.value[teamIdx];
|
||||
const memberId = team._selectedMemberId;
|
||||
if (!memberId) return;
|
||||
const participant = (eventModel.value.participants || []).find(p => p.id === memberId);
|
||||
if (participant && !team.members.some(m => m.id === participant.id)) {
|
||||
team.members.push({ id: participant.id, name: participant.Member?.name });
|
||||
}
|
||||
team._selectedMemberId = null;
|
||||
}
|
||||
|
||||
function getMemberAverage(member) {
|
||||
// 참가자 정보에서 Member.average 필드 사용
|
||||
if (!member.id) return null;
|
||||
const p = (eventModel.value.participants || []).find(x => x.id === member.id);
|
||||
if (!p) return null;
|
||||
if (p.Member && typeof p.Member.average === 'number') return p.Member.average;
|
||||
if (typeof p.average === 'number') return p.average; // 혹시 백엔드에서 이미 넣어주는 경우도 대비
|
||||
if (typeof p.averageScore === 'number') return p.averageScore;
|
||||
return null;
|
||||
}
|
||||
|
||||
function getTeamAverage(team) {
|
||||
const avgs = team.members.map(getMemberAverage).filter(v => typeof v === 'number');
|
||||
if (!avgs.length) return 0;
|
||||
return avgs.reduce((a, b) => a + b, 0) / avgs.length;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.team-generation-section {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.team-box {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||
}
|
||||
.event-details {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user