팀 생성 임시 완료
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<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="participants.length" placeholder="팀 인원수" class="mini-inputnumber" />
|
||||
<label class="ml-4">
|
||||
<input type="radio" value="byTeamCount" v-model="teamOption" /> 팀 개수로 나누기
|
||||
</label>
|
||||
<InputNumber v-if="teamOption==='byTeamCount'" v-model="teamOptionValue" :min="2" :max="participants.length" placeholder="팀 개수" class="mini-inputnumber" />
|
||||
<Button label="팀 생성" icon="pi pi-users" class="ml-3" :disabled="!canGenerateTeams" @click="generateTeams" />
|
||||
<Button label="팀 배정 저장" icon="pi pi-save" :disabled="teams.length === 0" @click="saveTeams" />
|
||||
</div>
|
||||
<!-- 미배정 인원 표시: 팀 리스트 위에 항상 노출 -->
|
||||
<div class="mt-3 mb-4">
|
||||
<strong>미배정 인원:</strong>
|
||||
<span v-if="unassignedMembers.length === 0">없음</span>
|
||||
<span v-else>
|
||||
<span v-for="member in unassignedMembers" :key="member.id" class="badge bg-gray-200 text-gray-800 mr-2">{{ member.name }}</span>
|
||||
</span>
|
||||
</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" style="width:80px;" />
|
||||
</div>
|
||||
<!-- 팀 멤버 드래그&드롭 정렬: ul로 감싸고, 각 아이템은 li로 시멘틱하게 구성 -->
|
||||
<ul class="pl-3">
|
||||
<draggable
|
||||
v-model="team.members"
|
||||
:group="'team-members-' + idx"
|
||||
item-key="id"
|
||||
handle=".drag-handle"
|
||||
@end="updateMemberOrder(idx)"
|
||||
>
|
||||
<template #item="{ element: member, index: mIdx }">
|
||||
<li class="flex align-items-center gap-2 mb-1 draggable-item">
|
||||
<span class="drag-handle" style="cursor:grab; color:#888"><i class="pi pi-bars"></i></span>
|
||||
<span class="badge bg-primary text-white mr-1">{{ mIdx + 1 }}</span>
|
||||
<span>{{ member.name }}</span>
|
||||
<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="removeMemberFromTeam(idx, mIdx)" v-if="team.members.length > 1" />
|
||||
</li>
|
||||
</template>
|
||||
</draggable>
|
||||
</ul>
|
||||
<div class="flex align-items-center gap-2 mt-2">
|
||||
<Dropdown
|
||||
v-model="team._selectedMember"
|
||||
:options="availableMembersForTeam(idx)"
|
||||
optionLabel="name"
|
||||
placeholder="팀원 선택"
|
||||
style="width: 180px;"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<span>{{ slotProps.option.name }}</span>
|
||||
<span v-if="getMemberAverage(slotProps.option) !== null" class="text-xs text-gray-700 ml-1">
|
||||
({{ getMemberAverage(slotProps.option) }})
|
||||
</span>
|
||||
</template>
|
||||
<template #value="slotProps">
|
||||
<span v-if="slotProps.value">
|
||||
{{ slotProps.value.name }}
|
||||
<span v-if="getMemberAverage(slotProps.value) !== null" class="text-xs text-gray-700 ml-1">
|
||||
({{ getMemberAverage(slotProps.value) }})
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<Button icon="pi pi-plus" class="p-button-text p-0" :disabled="!team._selectedMember" @click="addMemberToTeam(idx)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<strong>미배정 인원:</strong>
|
||||
<span v-if="unassignedMembers.length === 0">없음</span>
|
||||
<span v-else>
|
||||
<span v-for="member in unassignedMembers" :key="member.id" class="badge bg-gray-200 text-gray-800 mr-2">{{ member.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-gray-500 mt-3">팀을 생성하려면 옵션을 선택 후 '팀 생성'을 눌러주세요.</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import draggable from 'vuedraggable'; // npm install vuedraggable@next 필요 (vue3)
|
||||
|
||||
import { ref, computed, watch, unref } from 'vue';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Button from 'primevue/button';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
|
||||
// 참가자 에버리지 구하기
|
||||
function getMemberAverage(member) {
|
||||
if (!member.id) return null;
|
||||
// 원본 참가자 객체에서 id로 찾아서 평균값 추출
|
||||
const arr = unref(props.participants);
|
||||
const p = arr.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;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
participants: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
teams: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['teams-generated', 'save-teams', 'update:teams']);
|
||||
|
||||
const teamOption = ref('byMemberCount');
|
||||
const teamOptionValue = ref(2);
|
||||
const teams = ref(Array.isArray(props.teams) ? JSON.parse(JSON.stringify(props.teams)) : []);
|
||||
|
||||
// props.teams가 바뀌면 내부 상태 동기화
|
||||
watch(() => props.teams, (newVal) => {
|
||||
teams.value = Array.isArray(newVal) ? JSON.parse(JSON.stringify(newVal)) : [];
|
||||
}, { immediate: true, deep: true });
|
||||
|
||||
// teams가 내부에서 바뀌면 부모로 emit
|
||||
watch(teams, (val) => {
|
||||
emit('update:teams', val);
|
||||
}, { deep: true });
|
||||
|
||||
// 팀별로 아직 배정되지 않은 참가자만 선택지로 제공
|
||||
function availableMembersForTeam(teamIdx) {
|
||||
const arr = unref(props.participants);
|
||||
// 이미 다른 팀에 배정된 인원은 제외
|
||||
const assignedIds = new Set();
|
||||
teams.value.forEach((team, idx) => {
|
||||
if (idx === teamIdx) return;
|
||||
team.members.forEach(m => assignedIds.add(m.id));
|
||||
});
|
||||
// 현재 팀 내 중복도 막기 위해 현재 팀 내 id도 추가
|
||||
teams.value[teamIdx]?.members.forEach((m, i, arr) => {
|
||||
if (arr.findIndex(x => x.id === m.id) !== i) assignedIds.add(m.id);
|
||||
});
|
||||
return arr.filter(p => !assignedIds.has(p.id) && !teams.value[teamIdx].members.some(m => m.id === p.id)).map(p => ({ id: p.id, name: p.Member?.name || p.name || `참가자 ${p.id}` }));
|
||||
}
|
||||
|
||||
function addMemberToTeam(teamIdx) {
|
||||
const team = teams.value[teamIdx];
|
||||
const member = team._selectedMember;
|
||||
if (!member) return;
|
||||
// 구조 통일: 항상 {id, name}만 저장
|
||||
if (!team.members.some(m => m.id === member.id)) {
|
||||
team.members.push({ id: member.id, name: member.name });
|
||||
}
|
||||
team._selectedMember = null;
|
||||
}
|
||||
|
||||
function removeMemberFromTeam(teamIdx, memberIdx) {
|
||||
teams.value[teamIdx].members.splice(memberIdx, 1);
|
||||
}
|
||||
|
||||
// 미배정 인원 계산
|
||||
const unassignedMembers = computed(() => {
|
||||
const arr = unref(props.participants);
|
||||
const assignedIds = new Set();
|
||||
teams.value.forEach(team => {
|
||||
team.members.forEach(m => assignedIds.add(String(m.id)));
|
||||
});
|
||||
// name 보정: 항상 name 필드가 있도록!
|
||||
return arr
|
||||
.filter(p => !assignedIds.has(String(p.id)))
|
||||
.map(p => ({
|
||||
id: p.id,
|
||||
name: p.Member?.name || p.name || `참가자 ${p.id}`
|
||||
}));
|
||||
});
|
||||
|
||||
// 팀 생성/편집 다이얼로그 상태
|
||||
const editingTeam = ref({
|
||||
teamNumber: null,
|
||||
handicap: 0,
|
||||
members: []
|
||||
});
|
||||
const teamDialog = ref(false);
|
||||
const submitted = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
// 신규 팀 생성 다이얼로그 열기
|
||||
function openNewTeamDialog() {
|
||||
editingTeam.value = {
|
||||
teamNumber: null,
|
||||
handicap: 0,
|
||||
members: []
|
||||
};
|
||||
submitted.value = false;
|
||||
teamDialog.value = true;
|
||||
}
|
||||
|
||||
// 기존 팀 편집 다이얼로그 열기
|
||||
function editTeam(team) {
|
||||
editingTeam.value = {
|
||||
teamNumber: team.teamNumber,
|
||||
handicap: team.handicap,
|
||||
members: [...team.members]
|
||||
};
|
||||
submitted.value = false;
|
||||
teamDialog.value = true;
|
||||
}
|
||||
|
||||
// 팀 저장(생성/수정)
|
||||
async function saveTeam() {
|
||||
submitted.value = true;
|
||||
if (!editingTeam.value.teamNumber || editingTeam.value.members.length === 0) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
// 신규 생성 또는 기존 수정 구분(여기서는 teamNumber 기준)
|
||||
const idx = teams.value.findIndex(t => t.teamNumber === editingTeam.value.teamNumber);
|
||||
if (idx >= 0) {
|
||||
teams.value[idx] = { ...editingTeam.value };
|
||||
} else {
|
||||
teams.value.push({ ...editingTeam.value });
|
||||
}
|
||||
teamDialog.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 여러 팀 저장(상위로 emit)
|
||||
function saveTeams() {
|
||||
// 저장 시 members: [{id, order}], teamNumber: 1부터 순서대로 부여
|
||||
const payload = teams.value.map((team, idx) => ({
|
||||
...team,
|
||||
teamNumber: idx + 1,
|
||||
members: team.members.map((m, mIdx) => ({ id: m.id, order: mIdx + 1 }))
|
||||
}));
|
||||
emit('save-teams', payload);
|
||||
}
|
||||
|
||||
// 드래그 후 멤버 순서(order) 갱신
|
||||
function updateMemberOrder(teamIdx) {
|
||||
teams.value[teamIdx].members.forEach((m, i) => {
|
||||
m.order = i + 1;
|
||||
});
|
||||
}
|
||||
|
||||
const canGenerateTeams = computed(() => {
|
||||
const arr = unref(props.participants);
|
||||
const n = arr.length;
|
||||
if (n < 2) return false;
|
||||
return teamOptionValue.value >= 2 && teamOptionValue.value <= n;
|
||||
});
|
||||
|
||||
function shuffle(array) {
|
||||
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 arr = unref(props.participants);
|
||||
const participants = arr.map(p => ({
|
||||
id: p.id,
|
||||
name: p.Member?.name || p.name || `참가자 ${p.id}`
|
||||
}));
|
||||
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: [] });
|
||||
}
|
||||
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: [] });
|
||||
}
|
||||
shuffled.forEach((member, idx) => {
|
||||
result[idx % teamCount].members.push(member);
|
||||
});
|
||||
}
|
||||
teams.value = result;
|
||||
emit('teams-generated', result);
|
||||
}
|
||||
|
||||
watch(() => props.participants, () => {
|
||||
teams.value = [];
|
||||
}, { deep: true });
|
||||
</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);
|
||||
}
|
||||
.draggable-item {
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.draggable-item.dragging {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
.drag-handle {
|
||||
cursor: grab;
|
||||
font-size: 1.1em;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
min-width: 1.8em;
|
||||
padding: 0.2em 0.6em;
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
border-radius: 0.7em;
|
||||
background: #1976d2;
|
||||
color: #fff;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user