
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
// @/components/common/FileUploadProgress.vue <template>
<transition name="fade">
<div v-if="progress.isUploading" class="upload-modal-overlay">
<div class="upload-modal">
<div class="upload-modal-content">
<div class="upload-icon">
<!-- 파일 업로드 중 아이콘 -->
<svg v-if="currentStage === 'uploading'" width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 16V8M12 8L9 11M12 8L15 11" stroke="#4285F4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#4285F4" stroke-width="2" />
</svg>
<!-- 파일 다운로드 중 아이콘 -->
<svg v-else-if="currentStage === 'downloading'" width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 8V16M12 16L9 13M12 16L15 13" stroke="#4285F4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
<path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z" stroke="#4285F4" stroke-width="2" />
</svg>
<!-- 서버 처리 중 스피너 아이콘 -->
<svg v-else-if="currentStage === 'processing'" class="spinner" width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20Z" fill="#4CAF50" opacity="0.3" />
<path d="M12 2V4C16.41 4.0013 19.998 7.59252 20 12C20 16.41 16.41 20 12 20C7.59 20 4 16.41 4 12C4 10.15 4.63 8.45 5.69 7.1L4.07 5.87C2.66 7.63 1.99 9.78 2 12C2.05 17.57 6.53 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2Z" fill="#4CAF50" />
</svg>
</div>
<!-- 단계별 제목 -->
<h3>
<span v-if="currentStage === 'uploading'">파일 업로드 진행 중</span>
<span v-else-if="currentStage === 'downloading'">파일 다운로드 진행 중</span>
<span v-else-if="currentStage === 'processing'">서버 처리 중...</span>
</h3>
<!-- 파일 정보 -->
<div class="file-info">
<span class="file-name">{{ progress.currentFileName }}</span>
<span class="file-count">{{ progress.currentFileIndex }}/{{ progress.totalFiles }}</span>
</div>
<!-- 진행 바 (업로드/다운로드 중 또는 서버 처리 중에 따라 다른 진행 바 표시) -->
<div v-if="currentStage === 'uploading' || currentStage === 'downloading'" class="progress-bar-container">
<div class="progress-bar-fill" :class="{
'downloading': currentStage === 'downloading',
'simulated': isSimulated
}" :style="{ width: progress.totalProgress + '%' }"></div>
</div>
<div v-else-if="currentStage === 'processing'" class="progress-bar-container">
<!-- 서버 처리 중일 때는 processingProgress 사용 -->
<div class="progress-bar-fill processing" :style="{ width: progress.processingProgress + '%' }"></div>
</div>
<!-- 진행 상태 메시지 -->
<div class="progress-percentage">
<span v-if="currentStage === 'uploading'">{{ progress.totalProgress }}% 완료</span>
<span v-else-if="currentStage === 'downloading'">
<template v-if="progress.totalProgress <= 5">다운로드 요청 중...</template>
<template v-else-if="progress.totalProgress < 40 && isSimulated">다운로드 준비 중... {{ progress.totalProgress }}%</template>
<template v-else-if="progress.totalProgress < 40">다운로드 준비 완료</template>
<template v-else>{{ progress.totalProgress }}% 완료</template>
</span>
<span v-else-if="currentStage === 'processing' && progress.processingProgress < 100"> 처리 중... {{ progress.processingProgress }}% </span>
<span v-else-if="currentStage === 'processing' && progress.processingProgress === 100"> 처리 완료! </span>
</div>
</div>
</div>
</div>
</transition>
</template>
<script>
import uploadProgressStore from '@/resources/js/uploadProgressStore';
export default {
name: 'FileUploadProgress',
data() {
return {
progress: uploadProgressStore,
currentStage: 'uploading',
isSimulated: false,
// 진행률 변화 추적을 위한 변수
lastProgress: 0,
lastProgressUpdateTime: 0,
progressCheckTimer: null
};
},
created() {
// 초기 상태 설정
this.currentStage = this.progress.stage || 'uploading';
this.lastProgressUpdateTime = Date.now();
// 진행률 변화 감지 타이머 설정
this.progressCheckTimer = setInterval(() => {
const now = Date.now();
// 진행률이 정체되어 있으면 시뮬레이션 중으로 간주
if (this.progress.totalProgress === this.lastProgress) {
// 1초 이상 정체된 경우에만 시뮬레이션으로 판단
if (now - this.lastProgressUpdateTime > 1000) {
this.isSimulated = true;
}
} else {
// 진행률이 변경되면 타임스탬프 업데이트
this.lastProgressUpdateTime = now;
this.isSimulated = false;
}
this.lastProgress = this.progress.totalProgress;
}, 500);
},
beforeDestroy() {
// 타이머 정리
if (this.progressCheckTimer) {
clearInterval(this.progressCheckTimer);
}
},
watch: {
// progress.stage 변경 감시
'progress.stage'(newStage) {
this.currentStage = newStage;
},
// 진행률 변경 감시
'progress.totalProgress'(newValue, oldValue) {
if (newValue !== oldValue) {
this.lastProgressUpdateTime = Date.now();
// 진행률이 크게 변경된 경우 (실제 진행률 업데이트로 간주)
if (Math.abs(newValue - oldValue) > 5) {
this.isSimulated = false;
}
}
}
}
}
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.upload-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.upload-modal {
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
width: 360px;
padding: 0;
overflow: hidden;
}
.upload-modal-content {
padding: 24px;
display: flex;
flex-direction: column;
align-items: center;
}
.upload-icon {
margin-bottom: 16px;
}
/* 스피너 애니메이션 */
.spinner {
animation: spin 1.5s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
h3 {
font-size: 18px;
font-weight: 600;
margin: 0 0 16px 0;
color: #333;
}
.file-info {
width: 100%;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
font-size: 14px;
}
.file-name {
color: #555;
max-width: 240px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-count {
color: #777;
}
.progress-bar-container {
width: 100%;
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-bar-fill {
height: 100%;
background-color: #4285F4;
transition: width 0.3s ease;
}
/* 다운로드 중일 때 진행 바 스타일 */
.progress-bar-fill.downloading {
background-color: #34A853;
}
/* 시뮬레이션 중일 때 진행 바 스타일 */
.progress-bar-fill.simulated {
background: linear-gradient(90deg, #34A853 25%, #66BB6A 50%, #34A853 75%);
background-size: 200% 100%;
animation: loading 2s infinite linear;
}
/* 서버 처리 중일 때 진행 바 스타일 */
.progress-bar-fill.processing {
background: linear-gradient(90deg, #4CAF50 25%, #81C784 50%, #4CAF50 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite linear;
}
@keyframes loading {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.progress-percentage {
font-size: 14px;
color: #555;
font-weight: 500;
}
</style>