파일 업로드 수정
This commit is contained in:
@@ -22,61 +22,50 @@
|
|||||||
|
|
||||||
<!-- 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>
|
||||||
{{ uploadError }}
|
{{ uploadError }}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user