파일 업로드기능 완료
This commit is contained in:
@@ -312,7 +312,7 @@ const loadMenuItems = async () => {
|
||||
});
|
||||
|
||||
menuItems.value = groupMenus;
|
||||
|
||||
/*
|
||||
// 구독관리 메뉴 하드코딩으로 추가
|
||||
singleMenuItems.value = [
|
||||
...singleMenus,
|
||||
@@ -322,6 +322,7 @@ const loadMenuItems = async () => {
|
||||
to: '/club/subscription'
|
||||
}
|
||||
];
|
||||
*/
|
||||
} else {
|
||||
menuItems.value = [];
|
||||
singleMenuItems.value = [];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -61,9 +61,22 @@
|
||||
<span :class="scoreColorClass(scoreStates[member.participantId + '-' + n], scoresProxy[member.participantId + '-' + n], member.handicap)"
|
||||
class="score-hc-value score-status-text"
|
||||
:key="scoreStates[member.participantId + '-' + n] + '-' + scoresProxy[member.participantId + '-' + n]">
|
||||
{{ (scoresProxy[member.participantId + '-' + n] !== undefined && scoresProxy[member.participantId + '-' + n] !== '') ?
|
||||
Number(scoresProxy[member.participantId + '-' + n]) + (Number(member.handicap) || 0) : ''
|
||||
}}
|
||||
{{ (() => {
|
||||
const key = member.participantId + '-' + n;
|
||||
let score = scoresProxy[key];
|
||||
if (score === undefined || score === null || score === '') score = member.scores?.[n-1]?.score ?? 0;
|
||||
score = Number(score) || 0;
|
||||
// 핸디캡 우선순위: scoresProxy -handicap > member.scores[n-1]?.handicap > member.handicap
|
||||
let gameHandicap = null;
|
||||
if (scoresProxy[key + '-handicap'] !== undefined) {
|
||||
gameHandicap = Number(scoresProxy[key + '-handicap']) || 0;
|
||||
} else if (member.scores && member.scores[n-1] && typeof member.scores[n-1].handicap !== 'undefined') {
|
||||
gameHandicap = Number(member.scores[n-1].handicap) || 0;
|
||||
} else {
|
||||
gameHandicap = Number(member.handicap) || 0;
|
||||
}
|
||||
return score !== '' ? score + gameHandicap : '';
|
||||
})() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ import 'primeflex/primeflex.css'
|
||||
|
||||
// PrimeVue Components
|
||||
import Button from 'primevue/button'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
@@ -50,6 +51,9 @@ import Tag from 'primevue/tag'
|
||||
import FloatLabel from 'primevue/floatlabel';
|
||||
import Tooltip from 'primevue/tooltip';
|
||||
import Tabs from 'primevue/tabs';
|
||||
import FileUpload from 'primevue/fileupload';
|
||||
import Steps from 'primevue/steps';
|
||||
import Calendar from 'primevue/calendar';
|
||||
import TabPanels from 'primevue/tabpanels';
|
||||
import TabPanel from 'primevue/tabpanel';
|
||||
import TabList from 'primevue/tablist';
|
||||
@@ -79,6 +83,7 @@ app.component('AppHeader', AppHeader);
|
||||
|
||||
// Register PrimeVue Components
|
||||
app.component('Button', Button)
|
||||
app.component('SplitButton', SplitButton)
|
||||
app.component('InputText', InputText)
|
||||
app.component('DataTable', DataTable)
|
||||
app.component('Column', Column)
|
||||
@@ -107,5 +112,8 @@ app.component('ProgressSpinner', ProgressSpinner)
|
||||
app.component('Datepicker', Datepicker)
|
||||
app.component('Tag', Tag)
|
||||
app.component('FloatLabel', FloatLabel);
|
||||
app.component('FileUpload', FileUpload);
|
||||
app.component('Steps', Steps);
|
||||
app.component('Calendar', Calendar);
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -372,4 +372,122 @@ export default {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 파일 업로드 후 파싱 요청
|
||||
* @param {Object} data - 파일명과 클럽ID 정보
|
||||
* @returns {Promise<Object>} 파싱된 데이터
|
||||
*/
|
||||
async parseEventFile(data) {
|
||||
if (!data.filename || !data.clubId) {
|
||||
throw new Error('파일명과 클럽 ID가 필요합니다.');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/api/club/events/parse-preview', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
throw new Error(error.response.data.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 파일 업로드 URL 가져오기
|
||||
* @param {number} clubId - 클럽 ID
|
||||
* @returns {string} 파일 업로드 URL
|
||||
*/
|
||||
getFileUploadUrl(clubId) {
|
||||
if (!clubId) {
|
||||
throw new Error('클럽 ID가 필요합니다.');
|
||||
}
|
||||
return `/api/club/events/upload?clubId=${clubId}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* 파일 업로드 함수
|
||||
* @param {File} file - 업로드할 파일
|
||||
* @param {number} clubId - 클럽 ID
|
||||
* @returns {Promise<Object>} 업로드 결과
|
||||
*/
|
||||
async uploadFile(file, clubId) {
|
||||
if (!file || !clubId) {
|
||||
throw new Error('파일과 클럽 ID가 필요합니다.');
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('clubId', clubId);
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/api/club/events/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('파일 업로드 오류:', error);
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
throw new Error(error.response.data.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 이벤트 파일 데이터 저장
|
||||
* @param {Object} data - 이벤트 데이터, 참가자 데이터, 클럽 ID
|
||||
* @returns {Promise<Object>} 저장된 이벤트 정보
|
||||
*/
|
||||
async saveEventFileData(data) {
|
||||
if (!data.eventData || !data.participants || !data.clubId) {
|
||||
throw new Error('이벤트 정보, 참가자 정보, 클럽 ID가 필요합니다.');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/api/club/events/save-file-data', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
throw new Error(error.response.data.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 업로드된 임시 파일 삭제
|
||||
* @param {Object} data - 파일명 정보
|
||||
* @returns {Promise<Object>} 삭제 결과
|
||||
*/
|
||||
async deleteUploadedFile(data) {
|
||||
if (!data.filename) {
|
||||
throw new Error('파일명이 필요합니다.');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiClient.post('/api/club/events/delete-file', data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
throw new Error(error.response.data.message);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 이벤트 파일 업로드 URL 생성
|
||||
* @param {number} clubId - 클럽 ID
|
||||
* @returns {string} 파일 업로드 URL
|
||||
*/
|
||||
getFileUploadUrl(clubId) {
|
||||
if (!clubId) {
|
||||
throw new Error('클럽 ID가 필요합니다.');
|
||||
}
|
||||
return `/api/club/events/upload`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,8 +52,18 @@ export const calculateTeamGameScore = (team, scoresProxy, gameNumber) => {
|
||||
const score = scoresProxy[scoreKey];
|
||||
|
||||
if (score !== undefined && score !== null && score !== '') {
|
||||
const memberHc = Number(member.handicap) || 0;
|
||||
total += Number(score) + memberHc;
|
||||
// 핸디캡 우선순위: scoresProxy -handicap > member.scores[gameNumber-1]?.handicap > member.handicap
|
||||
let gameHandicap = null;
|
||||
|
||||
if (scoresProxy[scoreKey + '-handicap'] !== undefined) {
|
||||
gameHandicap = Number(scoresProxy[scoreKey + '-handicap']) || 0;
|
||||
} else if (member.scores && member.scores[gameNumber-1] && typeof member.scores[gameNumber-1].handicap !== 'undefined') {
|
||||
gameHandicap = Number(member.scores[gameNumber-1].handicap) || 0;
|
||||
} else {
|
||||
gameHandicap = Number(member.handicap) || 0;
|
||||
}
|
||||
|
||||
total += Number(score) + gameHandicap;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -96,7 +106,6 @@ export const calculateParticipantTotalScore = (participant, scoresProxy, gameCou
|
||||
if (!participant || !participant.participantId) return 0;
|
||||
|
||||
let total = 0;
|
||||
const handicap = Number(participant.handicap) || 0;
|
||||
|
||||
// 각 게임별 점수 합산
|
||||
for (let i = 1; i <= gameCount; i++) {
|
||||
@@ -104,7 +113,18 @@ export const calculateParticipantTotalScore = (participant, scoresProxy, gameCou
|
||||
const score = scoresProxy[scoreKey];
|
||||
|
||||
if (score !== undefined && score !== null && score !== '') {
|
||||
total += Number(score) + handicap;
|
||||
// 핸디캡 우선순위: scoresProxy -handicap > member.scores[i-1]?.handicap > member.handicap
|
||||
let gameHandicap = null;
|
||||
|
||||
if (scoresProxy[scoreKey + '-handicap'] !== undefined) {
|
||||
gameHandicap = Number(scoresProxy[scoreKey + '-handicap']) || 0;
|
||||
} else if (participant.scores && participant.scores[i-1] && typeof participant.scores[i-1].handicap !== 'undefined') {
|
||||
gameHandicap = Number(participant.scores[i-1].handicap) || 0;
|
||||
} else {
|
||||
gameHandicap = Number(participant.handicap) || 0;
|
||||
}
|
||||
|
||||
total += Number(score) + gameHandicap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<h2>이벤트 관리</h2>
|
||||
<div class="header-buttons">
|
||||
<Button label="캘린더 보기" icon="pi pi-calendar" class="p-button-outlined mr-2" @click="navigateToCalendar" />
|
||||
<Button label="파일에서 생성" icon="pi pi-file-import" class="p-button-outlined mr-2" @click="openFileUploadDialog" />
|
||||
<Button label="이벤트 추가" icon="pi pi-plus" @click="openNewEventDialog" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,16 +94,28 @@
|
||||
/>
|
||||
|
||||
<!-- 삭제 확인 다이얼로그 -->
|
||||
<Dialog v-model:visible="deleteEventDialog" :style="{ width: '450px' }" header="확인" :modal="true">
|
||||
<Dialog
|
||||
v-model:visible="deleteEventDialog"
|
||||
:style="{ width: '450px' }"
|
||||
header="이벤트 삭제"
|
||||
:modal="true"
|
||||
>
|
||||
<div class="confirmation-content">
|
||||
<i class="pi pi-exclamation-triangle mr-3" style="font-size: 2rem" />
|
||||
<span v-if="event">{{ event.title }} 이벤트를 삭제하시겠습니까?</span>
|
||||
<span>정말 이 이벤트를 삭제하시겠습니까?</span>
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="아니오" icon="pi pi-times" class="p-button-text" @click="deleteEventDialog = false" />
|
||||
<Button label="예" icon="pi pi-check" class="p-button-danger" @click="deleteEvent" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- 파일 업로드 다이얼로그 -->
|
||||
<FileUploadDialog
|
||||
v-model:visible="fileUploadDialog"
|
||||
:clubId="clubId"
|
||||
@event-created="onEventCreated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -111,6 +124,7 @@ 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';
|
||||
@@ -132,10 +146,12 @@ const clubStore = useClubStore();
|
||||
const events = ref([]);
|
||||
const event = ref({});
|
||||
const eventDialog = ref(false);
|
||||
const deleteEventDialog = ref(false);
|
||||
const detailsDialog = ref(false);
|
||||
const deleteEventDialog = ref(false);
|
||||
const fileUploadDialog = ref(false);
|
||||
const loading = ref(false);
|
||||
const submitted = ref(false);
|
||||
// localStorage에서 가져온 clubId를 숫자로 변환 시도
|
||||
const clubId = ref(localStorage.getItem('clubId') || null);
|
||||
|
||||
// 필터 상태
|
||||
@@ -434,6 +450,22 @@ const navigateToCalendar = () => {
|
||||
const url = `/club/calendar?clubId=${clubId.value}`;
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
// 파일 업로드 다이얼로그 열기
|
||||
const openFileUploadDialog = () => {
|
||||
fileUploadDialog.value = true;
|
||||
};
|
||||
|
||||
// 파일 업로드로 이벤트 생성 완료 처리
|
||||
const onEventCreated = async (eventData) => {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: '성공',
|
||||
detail: '파일에서 이벤트가 생성되었습니다.',
|
||||
life: 3000
|
||||
});
|
||||
await fetchEvents();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
</div>
|
||||
<div class="field col-6 mb-3">
|
||||
<label for="handicap" class="mb-1">핸디캡</label>
|
||||
<InputText id="handicap" v-model="member.handicap" type="number" min="0" max="50" class="w-full" />
|
||||
<InputText id="handicap" v-model="member.handicap" type="number" min="-300" max="300" class="w-full" />
|
||||
</div>
|
||||
<div class="field col-6 mb-3">
|
||||
<label for="status" class="mb-1">상태</label>
|
||||
|
||||
@@ -597,6 +597,29 @@ const sortedParticipants = computed(() => {
|
||||
// 점수 입력용 프록시 (v-model에서 계산식 사용 회피)
|
||||
const scoresProxy = ref({});
|
||||
|
||||
// 점수 입력 프록시를 숫자만으로 초기화
|
||||
function initScoresProxy() {
|
||||
if (!participants.value || !event.value?.gameCount) return;
|
||||
participants.value.forEach(member => {
|
||||
for (let n = 1; n <= event.value.gameCount; n++) {
|
||||
const key = member.participantId + '-' + n;
|
||||
// 객체 구조면 .score, 아니면 빈칸
|
||||
let score = '';
|
||||
if (member.scores && member.scores[n-1] && typeof member.scores[n-1] === 'object' && 'score' in member.scores[n-1]) {
|
||||
score = member.scores[n-1].score ?? '';
|
||||
} else if (member.scores && typeof member.scores[n-1] === 'number') {
|
||||
score = member.scores[n-1];
|
||||
}
|
||||
scoresProxy.value[key] = score;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// fetchEventData 등 데이터 로딩 후 반드시 호출
|
||||
watch([participants, () => event.value?.gameCount], () => {
|
||||
initScoresProxy();
|
||||
});
|
||||
|
||||
// 팀 미배정 인원 (백엔드 응답을 그대로 사용)
|
||||
const unassignedMembers = ref([]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user