팀 생성 임시 완료

This commit is contained in:
2025-04-26 07:06:03 +09:00
parent f01bcbdf39
commit d1b22d4272
20 changed files with 730 additions and 171 deletions
+19
View File
@@ -28,6 +28,7 @@
"vue": "^3.5.13",
"vue-chartjs": "^5.3.2",
"vue-router": "^4.5.0",
"vuedraggable": "^4.1.0",
"yup": "^1.3.3"
},
"devDependencies": {
@@ -3050,6 +3051,12 @@
}
}
},
"node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -3435,6 +3442,18 @@
"vue": "^3.2.0"
}
},
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"license": "MIT",
"dependencies": {
"sortablejs": "1.14.0"
},
"peerDependencies": {
"vue": "^3.0.1"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+1
View File
@@ -30,6 +30,7 @@
"vue": "^3.5.13",
"vue-chartjs": "^5.3.2",
"vue-router": "^4.5.0",
"vuedraggable": "^4.1.0",
"yup": "^1.3.3"
},
"devDependencies": {
+26 -6
View File
@@ -171,9 +171,9 @@ const fetchClubs = async () => {
}));
// 첫 번째 클럽의 memberType을 localStorage에 저장
if (clubs.value.length > 0) {
localStorage.setItem('userClubRole', clubs.value[0].memberType || '');
localStorage.setItem('clubId', clubs.value[0].id);
if (clubs.value.length > 0 && !selectedClubId.value) {
selectedClubId.value = clubs.value[0].id;
await setSelectedClub(clubs.value[0]);
}
} catch (error) {
console.error('클럽 목록 로드 실패:', error);
@@ -199,15 +199,30 @@ const fetchClubInfo = async () => {
}
};
// 클럽 선택
const setSelectedClub = async (club) => {
// 1. localStorage에 저장
localStorage.setItem('clubId', club.id);
localStorage.setItem('userClubRole', club.memberType || '');
// 2. 세션에도 저장 (백엔드에 clubId 전달)
try {
await apiClient.post('/api/club/select-club', { clubId: club.id });
} catch (error) {
console.error('클럽 ID 세션 저장 실패:', error);
}
// 3. 프론트엔드 상태도 변경
selectedClubId.value = club.id;
};
// 클럽 변경 시
const router = useRouter();
const onClubChange = async () => {
try {
// 선택된 클럽의 memberType 찾기
const selectedClub = clubs.value.find(club => club.id === selectedClubId.value);
if (selectedClub) {
localStorage.setItem('userClubRole', selectedClub.memberType || '');
localStorage.setItem('clubId', selectedClub.id);
await setSelectedClub(selectedClub);
}
await fetchClubInfo();
@@ -310,6 +325,11 @@ const handleUserLoggedIn = async () => {
// 컴포넌트 마운트 시 데이터 로드
onMounted(async () => {
await loadUserData();
const clubId = localStorage.getItem('clubId');
if (clubId) {
// 서버 세션에 clubId를 동기화
apiClient.post('/api/club/select-club', { clubId });
}
if (isLoggedIn.value) {
await fetchClubs();
await fetchClubInfo();
@@ -45,7 +45,7 @@
<ParticipantManagement
:eventId="eventModel.id"
:availableMembers="availableMembers"
@participant-updated="$emit('participant-updated')"
@participant-updated="handleParticipantUpdated"
/>
</TabPanel>
@@ -60,49 +60,7 @@
</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>
<TeamGeneration :participants="confirmedParticipants" :teams="teams" @update:teams="teams = $event" @teams-generated="teams = $event" @save-teams="handleSaveTeams" />
</TabPanel>
</TabView>
@@ -115,7 +73,8 @@
</template>
<script setup>
import { ref, computed } from 'vue';
import { ref, computed, watch } from 'vue';
import TeamGeneration from './TeamGeneration.vue';
import Dialog from 'primevue/dialog';
import TabView from 'primevue/tabview';
import TabPanel from 'primevue/tabpanel';
@@ -147,107 +106,68 @@ const formattedEndDate = computed(() => props.formatDate(eventModel.value.endDat
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;
// 이벤트 상세 다이얼로그가 열릴 때마다 팀 목록 불러오기
watch(() => eventModel.value.id, async (eventId) => {
if (eventId) {
const latestTeams = await teamService.getTeams(eventId);
teams.value = latestTeams;
}
}, { immediate: true });
// 참가확정 참가자 목록을 계산하는 computed 속성
const confirmedParticipants = computed(() => {
const arr = eventModel.value.EventParticipants?.filter(p => p.status === '참가확정') || [];
console.log('[상위] 참가확정:', arr);
return arr;
});
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]];
// 참가확정 인원 수
const confirmedCount = computed(() => confirmedParticipants.value.length);
// 팀 배정 저장 핸들러
import teamService from '@/services/teamService';
import { useToast } from 'primevue/usetoast';
const toast = useToast();
async function handleSaveTeams(savedTeams) {
const eventId = eventModel.value.id;
try {
await teamService.saveTeams(eventId, savedTeams);
toast.add({ severity: 'success', summary: '저장 완료', detail: '팀 배정이 저장되었습니다.', life: 2000 });
// 저장 후 최신 팀 목록 불러오기
const latestTeams = await teamService.getTeams(eventId);
teams.value = latestTeams;
} catch (e) {
toast.add({ severity: 'error', summary: '저장 실패', detail: '팀 저장 중 오류가 발생했습니다.', life: 3000 });
}
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);
});
// 참가자 상태 변경 처리 함수
function handleParticipantUpdated(data) {
// 부모 컴포넌트에 이벤트 전달
emit('participant-updated', data);
// 전체 참가자 목록이 있는 경우 직접 업데이트
if (data.allParticipants) {
// EventParticipants 속성을 완전히 대체
eventModel.value.EventParticipants = [...data.allParticipants];
}
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>
:deep(.mini-inputnumber .p-inputnumber-input) {
width: 60px !important;
min-width: 0 !important;
max-width: 100% !important;
}
</style>
<style scoped>
.team-generation-section {
margin-top: 1rem;
@@ -238,7 +238,15 @@ const saveParticipant = async () => {
});
}
// 참가자 데이터 다시 가져오기
await fetchParticipants();
// 참가자 변경 이벤트 발생 - 변경된 참가자와 전체 참가자 목록 함께 전달
emit('participant-updated', {
updatedParticipant: editingParticipant.value,
allParticipants: participants.value
});
hideDialog();
} catch (error) {
console.error('참가자 저장 오류:', error.message);
@@ -14,11 +14,6 @@
{{ slotProps.data.participant?.name }}
</template>
</Column>
<Column field="teamNumber" header="팀" sortable style="width: 80px">
<template #body="slotProps">
{{ slotProps.data.teamNumber }}
</template>
</Column>
<template v-for="i in maxGames" :key="i">
<Column :field="'games.' + (i-1) + '.score'" :header="i + '게임'" sortable>
<template #body="slotProps">
@@ -65,17 +60,6 @@
<small v-if="submitted && !editingScore.participantId" class="p-error">참가자를 선택해주세요.</small>
</div>
<div class="field">
<label for="teamNumber"></label>
<InputNumber
id="teamNumber"
v-model="editingScore.teamNumber"
:min="1"
:max="10"
placeholder="팀 번호"
/>
</div>
<div class="grid">
<template v-for="game in editingScore.games" :key="game.gameNumber">
<div class="col-6">
@@ -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>
+30
View File
@@ -0,0 +1,30 @@
import apiClient from './api';
const teamService = {
/**
* 이벤트별 팀 목록 조회
* @param {number|string} eventId
* @returns {Promise<Array>} 팀 배열
*/
async getTeams(eventId) {
const response = await apiClient.get(`/api/club/events/${eventId}/teams`);
return response.data.teams;
},
/**
* 이벤트별 팀 저장
* @param {number|string} eventId
* @param {Array} teams - [{ teamNumber, handicap, members: [id, ...] }]
*/
async saveTeams(eventId, teams) {
const payload = teams.map((team, idx) => ({
teamNumber: team.teamNumber ?? idx + 1,
handicap: team.handicap || 0,
members: team.members
}));
return apiClient.post(`/api/club/events/${eventId}/teams`, { teams: payload });
},
// 추후: 팀 불러오기, 수정, 삭제 등 추가 가능
};
export default teamService;
@@ -164,7 +164,6 @@ const fetchEvents = async () => {
loading.value = true;
try {
const response = await eventService.getEvents(clubId.value);
console.log('eventData', response);
events.value = response;
} catch (error) {
toast.add({