파일 업로드기능 완료

This commit is contained in:
2025-06-03 19:10:07 +09:00
parent e609a8ce52
commit eb0addf4e3
20 changed files with 4501 additions and 27 deletions
+2 -1
View File
@@ -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>
+8
View File
@@ -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')
+118
View File
@@ -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`;
}
};
+24 -4
View File
@@ -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;
}
}
+35 -3
View File
@@ -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>
+1 -1
View File
@@ -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([]);