파일 업로드 수정

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단계: 파일 업로드 -->
<div v-if="activeStep === 0" class="upload-step">
<div class="card">
<h3>파일 업로드</h3>
<p>엑셀(xlsx, xls), CSV, 이미지(jpg, png) 파일을 업로드하세요.<br>(이미지 파일은 정확성이 낮습니다. 가급적 엑셀 또는 CSV를 이용하세요.)</p>
<div class="file-upload-wrapper">
<FileUpload
ref="fileUploadRef"
name="file"
:url="uploadUrl"
@upload="onUpload"
@error="onError"
:auto="true"
chooseLabel="파일 선택"
uploadLabel="업로드"
cancelLabel="취소"
:maxFileSize="10000000"
accept=".xlsx,.xls,.csv,.jpg,.jpeg,.png"
:customUpload="true"
@uploader="uploadFile"
class="custom-file-upload"
>
<template #file="slotProps">
<div class="p-fileupload-row custom-file-item">
<div class="p-fileupload-filename">
{{slotProps.file.name}}
</div>
<div>
<span class="file-size">{{formatSize(slotProps.file.size)}}</span>
</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 class="file-upload-card">
<div class="file-upload-center">
<span class="upload-icon pi pi-cloud-upload"></span>
<div class="upload-text-group">
<div class="upload-title">파일 업로드</div>
<div class="upload-desc">
엑셀(xlsx, xls), CSV, 이미지(jpg, png) 파일을 업로드하세요.<br>
<span class="desc-note">(이미지 파일은 정확성이 낮습니다. 가급적 엑셀 또는 CSV를 이용하세요.)</span>
</div>
<label for="file-input" class="upload-btn">
파일을 선택하세요
<input
id="file-input"
type="file"
accept=".xlsx,.xls,.csv,.jpg,.jpeg,.png"
@change="onFileChange"
:disabled="loading || uploading"
style="display:none;"
/>
</label>
<div v-if="selectedFile" class="selected-file-info">
<span class="p-fileupload-filename">{{ selectedFile.name }}</span>
<span class="file-size">{{ formatSize(selectedFile.size) }}</span>
<span v-if="uploadStatus==='done'" class="p-tag p-tag-success ml-2">업로드 완료</span>
<span v-else-if="uploadStatus==='uploading'" class="p-tag p-tag-warning ml-2">업로드 </span>
<Button type="button" icon="pi pi-times" class="p-button-outlined p-button-rounded p-button-danger p-button-sm ml-2"
@click="removeSelectedFile" :disabled="uploading" />
</div>
</div>
</div>
<!-- 시트 선택 UI -->
<div v-if="sheetNames.length > 0" class="mt-3">
<!-- 시트 선택 UI - 시트가 여러 개일 경우에만 표시 -->
<div v-if="sheetNames.length > 1" class="mt-3">
<label for="sheet-select" class="field-label">엑셀 시트 선택</label>
<Select id="sheet-select" v-model="selectedSheetName" :options="sheetNames" placeholder="시트 선택" class="p-inputtext" />
<Button label="시트 데이터 불러오기" class="ml-2" @click="parseFile" :disabled="!selectedSheetName || loading" />
</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">
<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];
};
// 커스텀 파일 업로드 핸들러
const uploadFile = async (event) => {
// 파일이 없는 경우 처리
if (!event.files || event.files.length === 0) {
uploadError.value = '업로드할 파일을 선택해주세요.';
return;
}
// 단일 파일 업로드 상태
const selectedFile = ref(null);
const uploadStatus = ref('idle'); // idle | uploading | done
// 프론트에서 시트명 추출
const file = event.files[0];
const reader = new FileReader();
reader.onload = (e) => {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, { type: 'array' });
sheetNames.value = workbook.SheetNames;
// 기본값: 첫 번째 시트 선택
selectedSheetName.value = workbook.SheetNames[0] || '';
};
reader.readAsArrayBuffer(file);
const formData = new FormData();
formData.append('file', file);
formData.append('clubId', props.clubId);
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();
const onFileChange = (e) => {
const file = e.target.files[0];
if (file) {
selectedFile.value = file;
uploadStatus.value = 'idle';
// 엑셀 시트명 추출 (엑셀일 때만)
const ext = file.name.split('.').pop().toLowerCase();
if (["xlsx","xls"].includes(ext)) {
const reader = new FileReader();
reader.onload = (evt) => {
const data = new Uint8Array(evt.target.result);
const workbook = XLSX.read(data, { type: 'array' });
sheetNames.value = workbook.SheetNames;
selectedSheetName.value = workbook.SheetNames[0] || '';
};
reader.readAsArrayBuffer(file);
}
} catch (error) {
console.error('파일 업로드 오류:', error);
uploadError.value = '파일 업로드 중 오류가 발생했습니다.';
} finally {
loading.value = false;
uploading.value = false;
uploadSingleFile(); // 파일 선택 즉시 업로드
} else {
selectedFile.value = null;
uploadStatus.value = 'idle';
}
};
// 파일 업로드 완료 핸들러 (자동 업로드 사용 시)
const onUpload = (event) => {
const response = JSON.parse(event.xhr.response);
uploadedFile.value = response.file;
uploadError.value = '';
// 파일 파싱 요청
parseFile();
const removeSelectedFile = () => {
selectedFile.value = null;
uploadStatus.value = 'idle';
sheetNames.value = [];
selectedSheetName.value = '';
};
// 파일 업로드 오류 핸들러
const onError = (event) => {
uploadError.value = '파일 업로드 중 오류가 발생했습니다.';
const uploadedFilename = ref('');
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 () => {
if (!uploadedFile.value) {
if (!uploadedFilename.value) {
uploadError.value = '파일 업로드가 완료되지 않았습니다.';
return;
}
// 시트가 하나만 있을 경우 자동으로 선택
if (sheetNames.value.length === 1 && !selectedSheetName.value) {
selectedSheetName.value = sheetNames.value[0];
}
// 선택된 시트가 없으면 중단
if (!selectedSheetName.value) {
uploadError.value = '시트를 선택해주세요.';
return;
@@ -838,7 +824,7 @@ const parseFile = async () => {
loading.value = true;
try {
const response = await eventService.parseEventFile({
filename: uploadedFile.value.filename,
filename: uploadedFilename.value,
clubId: props.clubId,
sheetName: selectedSheetName.value
});
@@ -857,14 +843,26 @@ const parseFile = async () => {
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;
// 파일 파싱이 완료되면 파일 업로드 컴포넌트 초기화
if (fileUploadRef.value && fileUploadRef.value.clear) {
fileUploadRef.value.clear();
}
// 업로드 파일 정보는 유지 (파일명은 백엔드에서 사용하기 때문)
} catch (error) {
console.error('파일 파싱 오류:', error);
@@ -1350,6 +1348,69 @@ watch(() => props.visible, (newValue) => {
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 {
margin-left: 0.5rem;
}