파일 업로드 수정

This commit is contained in:
2025-06-10 10:52:04 +09:00
parent 0fb6d98421
commit 66a45eac8f
+164 -103
View File
@@ -22,60 +22,49 @@
<!-- 1단계: 파일 업로드 --> <!-- 1단계: 파일 업로드 -->
<div v-if="activeStep === 0" class="upload-step"> <div v-if="activeStep === 0" class="upload-step">
<div class="card"> <div class="file-upload-card">
<h3>파일 업로드</h3> <div class="file-upload-center">
<p>엑셀(xlsx, xls), CSV, 이미지(jpg, png) 파일을 업로드하세요.<br>(이미지 파일은 정확성이 낮습니다. 가급적 엑셀 또는 CSV를 이용하세요.)</p> <span class="upload-icon pi pi-cloud-upload"></span>
<div class="upload-text-group">
<div class="file-upload-wrapper"> <div class="upload-title">파일 업로드</div>
<FileUpload <div class="upload-desc">
ref="fileUploadRef" 엑셀(xlsx, xls), CSV, 이미지(jpg, png) 파일을 업로드하세요.<br>
name="file" <span class="desc-note">(이미지 파일은 정확성이 낮습니다. 가급적 엑셀 또는 CSV를 이용하세요.)</span>
:url="uploadUrl" </div>
@upload="onUpload" <label for="file-input" class="upload-btn">
@error="onError" 파일을 선택하세요
:auto="true" <input
chooseLabel="파일 선택" id="file-input"
uploadLabel="업로드" type="file"
cancelLabel="취소" accept=".xlsx,.xls,.csv,.jpg,.jpeg,.png"
:maxFileSize="10000000" @change="onFileChange"
accept=".xlsx,.xls,.csv,.jpg,.jpeg,.png" :disabled="loading || uploading"
:customUpload="true" style="display:none;"
@uploader="uploadFile" />
class="custom-file-upload" </label>
> <div v-if="selectedFile" class="selected-file-info">
<template #file="slotProps"> <span class="p-fileupload-filename">{{ selectedFile.name }}</span>
<div class="p-fileupload-row custom-file-item"> <span class="file-size">{{ formatSize(selectedFile.size) }}</span>
<div class="p-fileupload-filename"> <span v-if="uploadStatus==='done'" class="p-tag p-tag-success ml-2">업로드 완료</span>
{{slotProps.file.name}} <span v-else-if="uploadStatus==='uploading'" class="p-tag p-tag-warning ml-2">업로드 </span>
</div> <Button type="button" icon="pi pi-times" class="p-button-outlined p-button-rounded p-button-danger p-button-sm ml-2"
<div> @click="removeSelectedFile" :disabled="uploading" />
<span class="file-size">{{formatSize(slotProps.file.size)}}</span> </div>
</div> </div>
<div class="p-fileupload-badge">
<span class="p-tag p-tag-warning">Pending</span>
</div>
<div>
<Button type="button" icon="pi pi-times" class="p-button-outlined p-button-rounded p-button-danger p-button-sm"
@click="slotProps.remove()" />
</div>
</div>
</template>
<template #empty>
<div class="upload-placeholder">
<span class="upload-icon pi pi-cloud-upload"></span>
<p class="upload-title">파일을 여기에 끌어다 놓거나 클릭하여 선택하세요</p>
<p class="upload-note">지원 형식: Excel, CSV, JPG, PNG (최대 10MB)</p>
</div>
</template>
</FileUpload>
</div> </div>
<!-- 시트 선택 UI --> <!-- 시트 선택 UI - 시트가 여러 개일 경우에만 표시 -->
<div v-if="sheetNames.length > 0" class="mt-3"> <div v-if="sheetNames.length > 1" class="mt-3">
<label for="sheet-select" class="field-label">엑셀 시트 선택</label> <label for="sheet-select" class="field-label">엑셀 시트 선택</label>
<Select id="sheet-select" v-model="selectedSheetName" :options="sheetNames" placeholder="시트 선택" class="p-inputtext" /> <Select id="sheet-select" v-model="selectedSheetName" :options="sheetNames" placeholder="시트 선택" class="p-inputtext" />
<Button label="시트 데이터 불러오기" class="ml-2" @click="parseFile" :disabled="!selectedSheetName || loading" /> <Button label="시트 데이터 불러오기" class="ml-2" @click="parseFile" :disabled="!selectedSheetName || loading" />
</div> </div>
<!-- 시트가 하나만 있을 경우 자동으로 선택 -->
<div v-else-if="sheetNames.length === 1" class="mt-3">
<Button label="시트 데이터 불러오기" icon="pi pi-file-excel" class="p-button-raised" @click="parseFile" :disabled="loading" />
<small class="ml-2 text-500">시트: {{ sheetNames[0] }}</small>
</div>
<div v-if="uploadError" class="error-message"> <div v-if="uploadError" class="error-message">
<i class="pi pi-exclamation-triangle"></i> <i class="pi pi-exclamation-triangle"></i>
@@ -766,71 +755,68 @@ const formatSize = (bytes) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}; };
// 커스텀 파일 업로드 핸들러 // 단일 파일 업로드 상태
const uploadFile = async (event) => { const selectedFile = ref(null);
// 파일이 없는 경우 처리 const uploadStatus = ref('idle'); // idle | uploading | done
if (!event.files || event.files.length === 0) {
uploadError.value = '업로드할 파일을 선택해주세요.';
return;
}
// 프론트에서 시트명 추출 const onFileChange = (e) => {
const file = event.files[0]; const file = e.target.files[0];
const reader = new FileReader(); if (file) {
reader.onload = (e) => { selectedFile.value = file;
const data = new Uint8Array(e.target.result); uploadStatus.value = 'idle';
const workbook = XLSX.read(data, { type: 'array' }); // 엑셀 시트명 추출 (엑셀일 때만)
sheetNames.value = workbook.SheetNames; const ext = file.name.split('.').pop().toLowerCase();
// 기본값: 첫 번째 시트 선택 if (["xlsx","xls"].includes(ext)) {
selectedSheetName.value = workbook.SheetNames[0] || ''; const reader = new FileReader();
}; reader.onload = (evt) => {
reader.readAsArrayBuffer(file); const data = new Uint8Array(evt.target.result);
const workbook = XLSX.read(data, { type: 'array' });
const formData = new FormData(); sheetNames.value = workbook.SheetNames;
formData.append('file', file); selectedSheetName.value = workbook.SheetNames[0] || '';
formData.append('clubId', props.clubId); };
reader.readAsArrayBuffer(file);
try {
loading.value = true;
uploading.value = true;
const data = await eventService.uploadFile(file, props.clubId);
uploadedFile.value = data.file;
uploadError.value = '';
// 파일 파싱은 시트 선택 후 별도로 진행
// parseFile();
// 업로드 완료 후 파일 목록 초기화
if (event.clear) {
event.clear();
} }
} catch (error) { uploadSingleFile(); // 파일 선택 즉시 업로드
console.error('파일 업로드 오류:', error); } else {
uploadError.value = '파일 업로드 중 오류가 발생했습니다.'; selectedFile.value = null;
} finally { uploadStatus.value = 'idle';
loading.value = false;
uploading.value = false;
} }
}; };
// 파일 업로드 완료 핸들러 (자동 업로드 사용 시) const removeSelectedFile = () => {
const onUpload = (event) => { selectedFile.value = null;
const response = JSON.parse(event.xhr.response); uploadStatus.value = 'idle';
uploadedFile.value = response.file; sheetNames.value = [];
uploadError.value = ''; selectedSheetName.value = '';
// 파일 파싱 요청
parseFile();
}; };
// 파일 업로드 오류 핸들러 const uploadedFilename = ref('');
const onError = (event) => {
uploadError.value = '파일 업로드 중 오류가 발생했습니다.'; const uploadSingleFile = async () => {
if (!selectedFile.value) return;
uploadStatus.value = 'uploading';
try {
const res = await eventService.uploadFile(selectedFile.value, props.clubId);
// 반환 구조에 따라 아래 중 하나로 조정
uploadedFilename.value = res.file?.filename || res.filename;
uploadStatus.value = 'done';
} catch (e) {
uploadError.value = '파일 업로드 중 오류가 발생했습니다.';
uploadStatus.value = 'idle';
}
}; };
// 업로드된 파일 파싱 및 미리보기 데이터 로드 // 업로드된 파일 파싱 및 미리보기 데이터 로드
const parseFile = async () => { const parseFile = async () => {
if (!uploadedFile.value) { if (!uploadedFilename.value) {
uploadError.value = '파일 업로드가 완료되지 않았습니다.';
return; return;
} }
// 시트가 하나만 있을 경우 자동으로 선택
if (sheetNames.value.length === 1 && !selectedSheetName.value) {
selectedSheetName.value = sheetNames.value[0];
}
// 선택된 시트가 없으면 중단
if (!selectedSheetName.value) { if (!selectedSheetName.value) {
uploadError.value = '시트를 선택해주세요.'; uploadError.value = '시트를 선택해주세요.';
return; return;
@@ -838,7 +824,7 @@ const parseFile = async () => {
loading.value = true; loading.value = true;
try { try {
const response = await eventService.parseEventFile({ const response = await eventService.parseEventFile({
filename: uploadedFile.value.filename, filename: uploadedFilename.value,
clubId: props.clubId, clubId: props.clubId,
sheetName: selectedSheetName.value sheetName: selectedSheetName.value
}); });
@@ -857,14 +843,26 @@ const parseFile = async () => {
eventData.value.location = inferredInfo.location; eventData.value.location = inferredInfo.location;
} }
} }
// 파싱 완료 후 업로드된 파일 서버에서 삭제
if (uploadedFilename.value) {
try {
await eventService.deleteUploadedFile({ filename: uploadedFilename.value });
} catch (delErr) {
console.warn('업로드 파일 삭제 실패:', delErr);
}
}
// 업로드 폼 상태 초기화
selectedFile.value = null;
uploadedFilename.value = '';
uploadStatus.value = 'idle';
sheetNames.value = [];
selectedSheetName.value = '';
// 다음 단계로 이동 // 다음 단계로 이동
activeStep.value = 1; activeStep.value = 1;
// 파일 파싱이 완료되면 파일 업로드 컴포넌트 초기화 // 파일 파싱이 완료되면 파일 업로드 컴포넌트 초기화
if (fileUploadRef.value && fileUploadRef.value.clear) { if (fileUploadRef.value && fileUploadRef.value.clear) {
fileUploadRef.value.clear(); fileUploadRef.value.clear();
} }
// 업로드 파일 정보는 유지 (파일명은 백엔드에서 사용하기 때문) // 업로드 파일 정보는 유지 (파일명은 백엔드에서 사용하기 때문)
} catch (error) { } catch (error) {
console.error('파일 파싱 오류:', error); console.error('파일 파싱 오류:', error);
@@ -1350,6 +1348,69 @@ watch(() => props.visible, (newValue) => {
color: #6c757d; color: #6c757d;
} }
.file-upload-card {
background: #fff;
border-radius: 12px;
padding: 2.5rem 1.5rem 2rem 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: 300px;
margin-bottom: 1.5rem;
}
.file-upload-center {
display: flex;
flex-direction: column;
align-items: center;
}
.upload-icon {
font-size: 4rem;
color: #2196f3;
margin-bottom: 1.5rem;
}
.upload-text-group {
text-align: center;
}
.upload-title {
font-weight: bold;
font-size: 1.3rem;
margin-bottom: 0.5rem;
text-align: center;
}
.upload-desc {
color: #666;
font-size: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.upload-btn {
background: #2196f3;
color: #fff;
font-weight: 600;
padding: 0.75rem 1.5rem;
border-radius: 6px;
cursor: pointer;
font-size: 1.1rem;
transition: background 0.2s;
display: inline-block;
margin-bottom: 1.2rem;
}
.upload-btn:hover {
background: #1769aa;
}
.selected-file-info {
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.desc-note {
font-size: 0.9rem;
color: #999;
}
.ml-2 { .ml-2 {
margin-left: 0.5rem; margin-left: 0.5rem;
} }