박정하 박정하 05-21
250521 박정하 다운로드 수정
@4699a81a0ccc1ff191bfb4da6ac90d25ca646905
client/resources/api/dcry.js
--- client/resources/api/dcry.js
+++ client/resources/api/dcry.js
@@ -1,4 +1,5 @@
-import { apiClient, fileClient } from "./index";
+import { apiClient } from "./index";
+import uploadService from "@/resources/js/uploadService";
 
 // 기록물 목록 조회
 export const findDcrysProc = (searchReqDTO) => {
@@ -11,13 +12,13 @@
 }
 
 // 기록물 등록
-export const saveDcry = (formData) => {
-  return fileClient.post(`/dcry/saveDcry.file`, formData);
+export const saveDcryProc = (formData) => {
+  return uploadService.uploadWithPost(`/dcry/saveDcry.file`, formData);
 }
 
 // 기록물 수정
-export const updateDcry = (formData) => {
-  return fileClient.put(`/dcry/updateDcry.file`, formData);
+export const updateDcryProc = (formData) => {
+  return uploadService.uploadWithPut(`/dcry/updateDcry.file`, formData);
 }
 
 // 기록물 삭제
client/resources/api/file.js
--- client/resources/api/file.js
+++ client/resources/api/file.js
@@ -1,11 +1,11 @@
-import { apiClient } from "./index";
+import downloadService from "@/resources/js/downloadService";
 
 // 파일 다운로드
-export const fileDownloadProc = (files) => {
-  return apiClient.get(`/file/${files.fileId}/${files.fileOrdr}/fileDownload.json`, { responseType: 'blob' });
+export const fileDownloadProc = (file) => {
+  return downloadService.downloadFile(file, { responseType: 'blob' });
 }
 
-// 파일 다운로드
-export const multiFileDownloadProc = (fileIds) => {
-  return apiClient.post(`/file/multiFileDownload.json`, fileIds, { responseType: 'blob' });
+// 다중 파일 다운로드
+export const multiFileDownloadProc = (files) => {
+  return downloadService.downloadMultipleFiles(files, { responseType: 'blob' });
 }
(파일 끝에 줄바꿈 문자 없음)
client/resources/api/nesDta.js
--- client/resources/api/nesDta.js
+++ client/resources/api/nesDta.js
@@ -1,4 +1,6 @@
-import { apiClient, fileClient } from "./index";
+// @/resources/api/nesDta.js
+import { apiClient } from "./index";
+import uploadService from "@/resources/js/uploadService";
 
 // 스크랩 자료 목록 조회
 export const findAllNesDtasProc = (searchReqDTO) => {
@@ -10,14 +12,14 @@
   return apiClient.get(`/nesDta/${nesDtaId}/findNesDta.json`);
 }
 
-// 스크랩 자료 등록
+// 스크랩 자료 등록 (POST 메서드 사용)
 export const saveNesDtaProc = (formData) => {
-  return fileClient.post(`/nesDta/saveNesDta.file`, formData);
+  return uploadService.uploadWithPost(`/nesDta/saveNesDta.file`, formData);
 }
 
-// 스크랩 자료 수정
+// 스크랩 자료 수정 (PUT 메서드 사용)
 export const updateNesDtaProc = (formData) => {
-  return fileClient.put(`/nesDta/updateNesDta.file`, formData);
+  return uploadService.uploadWithPut(`/nesDta/updateNesDta.file`, formData);
 }
 
 // 스크랩 자료 삭제
 
client/resources/js/downloadService.js (added)
+++ client/resources/js/downloadService.js
@@ -0,0 +1,190 @@
+// @/resources/js/downloadService.js
+import { apiClient } from '@/resources/api/index';
+import uploadProgressStore from '@/resources/js/uploadProgressStore';
+
+// 파일 다운로드를 처리하는 서비스
+const downloadService = {
+  // 내부 타이머 변수
+  _simulationTimer: null,
+
+  // 마지막 시뮬레이션 진행률
+  _lastSimulatedProgress: 0,
+
+  // 진행률 시뮬레이션 시작 (서버 응답이 지연될 때)
+  _startProgressSimulation() {
+    // 이미 타이머가 있으면 정리
+    if (this._simulationTimer) {
+      clearInterval(this._simulationTimer);
+    }
+
+    // 현재 진행률 저장
+    this._lastSimulatedProgress = uploadProgressStore.totalProgress;
+
+    // 시작 진행률 설정 (최소 5%)
+    if (this._lastSimulatedProgress < 5) {
+      this._lastSimulatedProgress = 5;
+      uploadProgressStore.totalProgress = 5;
+    }
+
+    // 진행률 시뮬레이션 시작 (최대 40%까지만)
+    let simulatedProgress = this._lastSimulatedProgress;
+    this._simulationTimer = setInterval(() => {
+      // 실제 진행이 시작되면 시뮬레이션 중단
+      if (uploadProgressStore.totalProgress > simulatedProgress) {
+        this._lastSimulatedProgress = uploadProgressStore.totalProgress;
+        clearInterval(this._simulationTimer);
+        this._simulationTimer = null;
+        return;
+      }
+
+      // 진행률 증가 (40%까지)
+      if (simulatedProgress < 40) {
+        simulatedProgress += Math.random() * 1.5; // 0~1.5% 랜덤하게 증가
+        uploadProgressStore.totalProgress = Math.round(simulatedProgress);
+        this._lastSimulatedProgress = uploadProgressStore.totalProgress;
+      }
+    }, 200); // 0.2초마다 업데이트
+  },
+
+  // 시뮬레이션 종료
+  _stopProgressSimulation() {
+    if (this._simulationTimer) {
+      clearInterval(this._simulationTimer);
+      this._simulationTimer = null;
+    }
+  },
+
+  // 단일 파일 다운로드 메서드
+  async downloadFile(file, options = {}) {
+    try {
+      // 다운로드 상태 초기화 (현재 진행률 유지)
+      const currentProgress = uploadProgressStore.totalProgress;
+      uploadProgressStore.startDownload(file.fileNm || '파일 다운로드 중...');
+      uploadProgressStore.setStage('downloading');
+
+      // 이전 진행률이 있으면 유지
+      if (currentProgress > 0) {
+        uploadProgressStore.totalProgress = currentProgress;
+      }
+
+      // 즉시 진행률 시뮬레이션 시작 (서버 응답 전)
+      this._startProgressSimulation();
+
+      // apiClient를 사용하여 다운로드 요청
+      const response = await apiClient.get(`/file/${file.fileId}/${file.fileOrdr || 1}/fileDownload.json`, {
+        responseType: 'blob',
+        ...options,
+        onDownloadProgress: (progressEvent) => {
+          // 시뮬레이션 중단
+          this._stopProgressSimulation();
+
+          if (progressEvent.total) {
+            // 진행 상태 계산
+            const realProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+
+            // 실제 진행률이 시뮬레이션 진행률보다 낮으면, 시뮬레이션 진행률부터 시작
+            const startProgress = Math.max(this._lastSimulatedProgress, realProgress);
+
+            // 남은 진행률을 60%~100% 범위로 매핑
+            // 예: 실제 진행률이 0%일 때 40%, 100%일 때 100%가 되도록
+            const adjustedProgress = 40 + (realProgress * 60 / 100);
+
+            // 둘 중 더 큰 값을 사용
+            uploadProgressStore.totalProgress = Math.round(Math.max(startProgress, adjustedProgress));
+          }
+
+          // 사용자 정의 onDownloadProgress 콜백이 있으면 호출
+          if (options.onDownloadProgress) {
+            options.onDownloadProgress(progressEvent);
+          }
+        }
+      });
+
+      // 시뮬레이션 중단 (만약 아직 실행 중이라면)
+      this._stopProgressSimulation();
+
+      // 다운로드 완료 표시
+      uploadProgressStore.totalProgress = 100;
+
+      // 잠시 후 응답 반환 (완료 상태를 보여줄 시간 제공)
+      await new Promise(resolve => setTimeout(resolve, 300));
+
+      return response;
+    } catch (error) {
+      // 시뮬레이션 중단
+      this._stopProgressSimulation();
+
+      // 오류 발생 시 상태 초기화
+      uploadProgressStore.handleError();
+      throw error;
+    }
+  },
+
+  // 다중 파일 다운로드 메서드
+  async downloadMultipleFiles(files, options = {}) {
+    try {
+      // 다운로드 상태 초기화 (현재 진행률 유지)
+      const currentProgress = uploadProgressStore.totalProgress;
+      uploadProgressStore.startDownload('다중 파일 다운로드 중...');
+      uploadProgressStore.setStage('downloading');
+
+      // 이전 진행률이 있으면 유지
+      if (currentProgress > 0) {
+        uploadProgressStore.totalProgress = currentProgress;
+      }
+
+      // 즉시 진행률 시뮬레이션 시작 (서버 응답 전)
+      this._startProgressSimulation();
+
+      // apiClient를 사용하여 다운로드 요청
+      const response = await apiClient.post(`/file/multiFileDownload.json`, files, {
+        responseType: 'blob',
+        ...options,
+        onDownloadProgress: (progressEvent) => {
+          // 시뮬레이션 중단
+          this._stopProgressSimulation();
+
+          if (progressEvent.total) {
+            // 진행 상태 계산
+            const realProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total);
+
+            // 실제 진행률이 시뮬레이션 진행률보다 낮으면, 시뮬레이션 진행률부터 시작
+            const startProgress = Math.max(this._lastSimulatedProgress, realProgress);
+
+            // 남은 진행률을 40%~100% 범위로 매핑
+            // 예: 실제 진행률이 0%일 때 40%, 100%일 때 100%가 되도록
+            const adjustedProgress = 40 + (realProgress * 60 / 100);
+
+            // 둘 중 더 큰 값을 사용
+            uploadProgressStore.totalProgress = Math.round(Math.max(startProgress, adjustedProgress));
+          }
+
+          // 사용자 정의 onDownloadProgress 콜백이 있으면 호출
+          if (options.onDownloadProgress) {
+            options.onDownloadProgress(progressEvent);
+          }
+        }
+      });
+
+      // 시뮬레이션 중단 (만약 아직 실행 중이라면)
+      this._stopProgressSimulation();
+
+      // 다운로드 완료 표시
+      uploadProgressStore.totalProgress = 100;
+
+      // 잠시 후 응답 반환 (완료 상태를 보여줄 시간 제공)
+      await new Promise(resolve => setTimeout(resolve, 300));
+
+      return response;
+    } catch (error) {
+      // 시뮬레이션 중단
+      this._stopProgressSimulation();
+
+      // 오류 발생 시 상태 초기화
+      uploadProgressStore.handleError();
+      throw error;
+    }
+  }
+};
+
+export default downloadService;(파일 끝에 줄바꿈 문자 없음)
 
client/resources/js/uploadProgressStore.js (added)
+++ client/resources/js/uploadProgressStore.js
@@ -0,0 +1,133 @@
+import { reactive } from 'vue';
+
+// 파일 업로드/다운로드 상태를 관리하는 전역 스토어
+const uploadProgressStore = reactive({
+  // 상태 데이터
+  isUploading: false,  // 모달 표시 여부
+  currentFileIndex: 0,
+  totalFiles: 0,
+  currentFileName: '',
+  totalProgress: 0,
+  stage: '', // 'uploading', 'processing', 'downloading'
+  processingProgress: 0, // 서버 처리 단계의 가상 진행률 (0-100)
+
+  // 상태 초기화 메서드
+  resetState() {
+    this.isUploading = false;
+    this.currentFileIndex = 0;
+    this.totalFiles = 0;
+    this.currentFileName = '';
+    this.totalProgress = 0;
+    this.stage = '';
+    this.processingProgress = 0;
+
+    // 진행 타이머가 있다면 정리
+    if (this._processingTimer) {
+      clearInterval(this._processingTimer);
+      this._processingTimer = null;
+    }
+  },
+
+  // 업로드 시작 메서드
+  startUpload(fileName, totalCount = 1) {
+    // 혹시 모를 이전 상태 초기화
+    this.resetState();
+
+    // 새 업로드 시작
+    this.isUploading = true;
+    this.currentFileIndex = 1;
+    this.totalFiles = totalCount;
+    this.currentFileName = fileName;
+    this.totalProgress = 0;
+    this.stage = 'uploading';
+  },
+
+  // 다운로드 시작 메서드
+  startDownload(fileName) {
+    // 혹시 모를 이전 상태 초기화
+    this.resetState();
+
+    // 새 다운로드 시작
+    this.isUploading = true;
+    this.currentFileIndex = 1;
+    this.totalFiles = 1;
+    this.currentFileName = fileName;
+    this.totalProgress = 0;
+    this.stage = 'downloading';
+  },
+
+  // 진행 상태 업데이트 메서드
+  updateProgress(loaded, total) {
+    if (total > 0) {
+      const progress = Math.round((loaded * 100) / total);
+      this.totalProgress = progress;
+    }
+  },
+
+  // 내부용 타이머 변수
+  _processingTimer: null,
+
+  // 업로드/다운로드 단계 설정 메서드
+  setStage(stage) {
+    this.stage = stage;
+
+    if (stage === 'uploading' || stage === 'downloading') {
+      // 업로드/다운로드 단계로 변경 시 처리 단계 타이머 정리
+      if (this._processingTimer) {
+        clearInterval(this._processingTimer);
+        this._processingTimer = null;
+      }
+      this.processingProgress = 0;
+    }
+    else if (stage === 'processing') {
+      // 파일 업로드는 100% 완료
+      this.totalProgress = 100;
+
+      // 처리 단계 진행 시뮬레이션 시작
+      this.processingProgress = 0;
+
+      // 타이머가 이미 있으면 정리
+      if (this._processingTimer) {
+        clearInterval(this._processingTimer);
+      }
+
+      // 처리 단계 진행 시뮬레이션을 위한 타이머 설정
+      // 최대 95%까지만 채워서 실제 완료 전에는 100%가 되지 않도록 함
+      this._processingTimer = setInterval(() => {
+        if (this.processingProgress < 95) {
+          // 처음에는 빠르게, 나중에는 천천히 증가하도록 설정
+          const increment = Math.max(1, 10 - Math.floor(this.processingProgress / 10));
+          this.processingProgress += increment;
+        } else {
+          // 95%에 도달하면 타이머 정지
+          clearInterval(this._processingTimer);
+          this._processingTimer = null;
+        }
+      }, 500); // 0.5초마다 업데이트
+    }
+  },
+
+  // 서버 처리 완료 메서드 (API 응답 수신 후 호출)
+  completeProcessing() {
+    // 처리 타이머 정리
+    if (this._processingTimer) {
+      clearInterval(this._processingTimer);
+      this._processingTimer = null;
+    }
+
+    // 처리 진행률 100%로 설정
+    this.processingProgress = 100;
+  },
+
+  // 모달 닫기 메서드 (alert 표시 후 호출)
+  closeModal() {
+    this.resetState();
+  },
+
+  // 오류 발생 시 호출할 메서드
+  handleError() {
+    this.resetState();
+  }
+});
+
+export default uploadProgressStore;(파일 끝에 줄바꿈 문자 없음)
 
client/resources/js/uploadService.js (added)
+++ client/resources/js/uploadService.js
@@ -0,0 +1,109 @@
+import { fileClient } from '@/resources/api/index';
+import uploadProgressStore from '@/resources/js/uploadProgressStore';
+
+// 파일 업로드를 처리하는 서비스
+const uploadService = {
+  // POST 메서드를 사용한 업로드 (등록)
+  async uploadWithPost(url, formData, options = {}) {
+    // 파일 정보 추출
+    const file = formData.get('multipartFiles') || formData.get('file');
+
+    if (file) {
+      // 업로드 상태 초기화 및 파일 전송 단계 시작 (이 초기화는 유지)
+      uploadProgressStore.startUpload(file.name, 1);
+    }
+
+    try {
+      // 1단계: 파일 전송 단계
+      uploadProgressStore.setStage('uploading');
+
+      // fileClient.post를 사용하여 업로드 요청
+      const response = await fileClient.post(url, formData, {
+        ...options,
+        onUploadProgress: (progressEvent) => {
+          // 진행 상태 업데이트
+          uploadProgressStore.updateProgress(progressEvent.loaded, progressEvent.total);
+
+          // 사용자 정의 onUploadProgress 콜백이 있으면 호출
+          if (options.onUploadProgress) {
+            options.onUploadProgress(progressEvent);
+          }
+
+          // 업로드가 완료되면 처리 중 단계로 전환
+          if (progressEvent.loaded === progressEvent.total) {
+            // setTimeout을 사용하여 처리 단계 전환을 강제로 비동기 실행
+            setTimeout(() => {
+              uploadProgressStore.setStage('processing');
+            }, 100);
+          }
+        }
+      });
+
+      // 서버 응답 수신 - 처리 완료 표시
+      uploadProgressStore.completeProcessing();
+
+      // 잠시 후 응답 반환 (처리 완료 상태를 보여줄 시간 제공)
+      await new Promise(resolve => setTimeout(resolve, 500));
+
+      // 응답 반환 (alert은 API 함수에서 처리)
+      return response;
+    } catch (error) {
+      // 오류 발생 시 상태 초기화
+      uploadProgressStore.handleError();
+      throw error; // 오류 다시 throw
+    }
+  },
+
+  // PUT 메서드를 사용한 업로드 (수정)
+  async uploadWithPut(url, formData, options = {}) {
+    // 파일 정보 추출
+    const file = formData.get('multipartFiles') || formData.get('file');
+
+    if (file) {
+      // 업로드 상태 초기화 및 파일 전송 단계 시작 (이 초기화는 유지)
+      uploadProgressStore.startUpload(file.name, 1);
+    }
+
+    try {
+      // 1단계: 파일 전송 단계
+      uploadProgressStore.setStage('uploading');
+
+      // fileClient.put을 사용하여 업로드 요청
+      const response = await fileClient.put(url, formData, {
+        ...options,
+        onUploadProgress: (progressEvent) => {
+          // 진행 상태 업데이트
+          uploadProgressStore.updateProgress(progressEvent.loaded, progressEvent.total);
+
+          // 사용자 정의 onUploadProgress 콜백이 있으면 호출
+          if (options.onUploadProgress) {
+            options.onUploadProgress(progressEvent);
+          }
+
+          // 업로드가 완료되면 처리 중 단계로 전환
+          if (progressEvent.loaded === progressEvent.total) {
+            // setTimeout을 사용하여 처리 단계 전환을 강제로 비동기 실행
+            setTimeout(() => {
+              uploadProgressStore.setStage('processing');
+            }, 100);
+          }
+        }
+      });
+
+      // 서버 응답 수신 - 처리 완료 표시
+      uploadProgressStore.completeProcessing();
+
+      // 잠시 후 응답 반환 (처리 완료 상태를 보여줄 시간 제공)
+      await new Promise(resolve => setTimeout(resolve, 500));
+
+      // 응답 반환 (alert은 API 함수에서 처리)
+      return response;
+    } catch (error) {
+      // 오류 발생 시 상태 초기화
+      uploadProgressStore.handleError();
+      throw error; // 오류 다시 throw
+    }
+  }
+};
+
+export default uploadService;(파일 끝에 줄바꿈 문자 없음)
client/views/App.vue
--- client/views/App.vue
+++ client/views/App.vue
@@ -1,25 +1,40 @@
 <template>
- <div class="wrapper ">
+  <div class="wrapper ">
     <Header />
-    <div class="container "><router-view /> 
-      <button class="scroll-up"  @click="scrollToTop">
-      <img src="../resources/images/icon/top.png" alt="">
-    </button>
+    <div class="container "><router-view />
+      <button class="scroll-up" @click="scrollToTop">
+        <img src="../resources/images/icon/top.png" alt="">
+      </button>
     </div>
-  
   </div>
   <Footer />
+  <file-upload-progress />
 </template>
 <script>
 import Header from './layout/Header.vue';
 import Footer from './layout/Footer.vue';
 
-export default {
+import FileUploadProgress from '@/views/component/FileUploadProgress.vue';
+import uploadProgressStore from '@/resources/js/uploadProgressStore';
 
+export default {
   components: {
-    Header: Header,
-    Footer: Footer,
+    Header,
+    Footer,
+    FileUploadProgress,
   },
+
+  created() {
+    // 앱 초기화 시 업로드 상태 초기화
+    uploadProgressStore.resetState();
+
+    // 라우터 변경 시 업로드 상태 초기화
+    this.$router.beforeEach((to, from, next) => {
+      uploadProgressStore.resetState();
+      next();
+    });
+  },
+
   async mounted() {
     // Access Token이 없거나 만료된 경우 새로 발급 요청
     const token = this.$store.state.authorization; // Vuex 스토어에서 직접 가져오기
@@ -27,6 +42,7 @@
       await this.refreshToken();
     }
   },
+
   methods: {
     async refreshToken() {
       try {
@@ -58,6 +74,7 @@
         this.$router.push('/login.page'); // 로그인 페이지로 리다이렉트
       }
     },
+
     scrollToTop() {
       window.scrollTo({
         top: 0,
@@ -66,5 +83,4 @@
     },
   },
 }
-</script>
-<style></style>
(파일 끝에 줄바꿈 문자 없음)
+</script>
(파일 끝에 줄바꿈 문자 없음)
 
client/views/component/FileUploadProgress.vue (added)
+++ client/views/component/FileUploadProgress.vue
@@ -0,0 +1,267 @@
+// @/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>(파일 끝에 줄바꿈 문자 없음)
client/views/component/player/VideoComponent.vue
--- client/views/component/player/VideoComponent.vue
+++ client/views/component/player/VideoComponent.vue
@@ -5,7 +5,9 @@
       <p>이 비디오 형식은 웹 브라우저에서 직접 재생할 수 없습니다. 다운로드 버튼을 이용해 원본 파일을 다운로드하세요.</p>
     </div>
     <!-- 비디오 플레이어 -->
-    <video ref="videoPlayer" class="video-element" :src="videoUrl" controls @error="onError"></video>
+    <!-- <video ref="videoPlayer" class="video-element" :src="videoUrl" controls @error="onError"></video> -->
+    <video ref="videoPlayer" class="video-element" :src="videoUrl" controls controlsList="nodownload" oncontextmenu="return false;" @error="onError">
+    </video>
   </div>
 </template>
 <script>
client/views/pages/AppRouter.js
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
@@ -1,5 +1,7 @@
-import { createWebHistory, createRouter } from "vue-router";
 import store from "./AppStore";
+import uploadProgressStore from '@/resources/js/uploadProgressStore';
+import { createWebHistory, createRouter } from "vue-router";
+
 import { updateStatsByMenuId } from "../../resources/api/main";
 
 // 공통
@@ -165,6 +167,9 @@
     }
   }
 
+  // 모든 페이지 이동 시 업로드 상태 초기화
+  uploadProgressStore.resetState();
+
   next();
 });
 
client/views/pages/bbsDcryPhoto/PicHistoryDetail.vue
--- client/views/pages/bbsDcryPhoto/PicHistoryDetail.vue
+++ client/views/pages/bbsDcryPhoto/PicHistoryDetail.vue
@@ -38,7 +38,7 @@
                   <img :src="item.filePath" :alt="item.fileNm" />
                 </figure>
                 <div class="float-div">
-                  <button type="button" class="btn-sm red" @click="fnDownload('selected', item)">다운로드</button>
+                  <button type="button" class="btn-sm red" @click="fnDownload('single', item)">다운로드</button>
                 </div>
               </div>
             </div>
@@ -82,53 +82,18 @@
       <button class="gray-line-bg " type="button" @click="fnMoveTo('list')">목록</button>
     </div>
   </div>
-  <div v-if="loading" class="loading-overlay">
-    <div class="loading-spinner"></div>
-    <div>
-      <p>다운로드 중입니다</p>
-      <p>잠시만 기다려주세요</p>
-    </div>
-  </div>
 </template>
 <script>
-import { ref } from 'vue';
-// Import Swiper Vue components
-import { CaretRightOutlined, PauseOutlined } from '@ant-design/icons-vue';
-import { Swiper, SwiperSlide } from 'swiper/vue';
-// Import Swiper styles
-import 'swiper/css';
-import 'swiper/css/free-mode';
-import 'swiper/css/navigation';
-import 'swiper/css/thumbs';
-// import required modules
-import { FreeMode, Navigation, Thumbs } from 'swiper/modules';
 // COMPONENT
 import ViewerComponent from '@/views/component/editor/ViewerComponent.vue';
 // API
+import uploadProgressStore from '@/resources/js/uploadProgressStore';
 import { findDcryProc, deleteDcryProc } from '@/resources/api/dcry';
 import { fileDownloadProc, multiFileDownloadProc } from '@/resources/api/file';
 
 export default {
   components: {
-    PauseOutlined,
-    CaretRightOutlined,
-    Swiper,
-    SwiperSlide,
     ViewerComponent,
-  },
-
-  setup() {
-    const thumbsSwiper = ref(null);
-
-    const setThumbsSwiper = (swiper) => {
-      thumbsSwiper.value = swiper;
-    };
-
-    return {
-      thumbsSwiper,
-      setThumbsSwiper,
-      modules: [FreeMode, Navigation, Thumbs],
-    };
   },
 
   data() {
@@ -175,10 +140,6 @@
       this.currentImg = img;
     },
 
-    handleMainSlideChange(swiper) {
-      this.activeIndex = swiper.realIndex;
-    },
-
     // 상세 조회
     async fnFindDcry() {
       try {
@@ -206,14 +167,12 @@
 
     // 파일 다운로드
     async fnDownload(type, file) {
-      this.loading = true;
-
       let url = null;
       let link = null;
 
       try {
         let fileList = [];
-        if (type === 'selected') {
+        if (type === 'single') {
           fileList.push(file);
         } else if (type === 'all') {
           fileList = this.findResult.files;
@@ -222,35 +181,45 @@
         let isMultiple = fileList.length > 1;
         let files = isMultiple ? fileList : fileList[0];
 
-        // Call the API to get the file data
         const response = isMultiple ? await multiFileDownloadProc(files) : await fileDownloadProc(files);
 
-        // 파일명 추출 부분 수정
-        let filename = isMultiple ? 'downloadFile.zip' : 'downloadFile.bin';
-        const disposition = response.headers['content-disposition'];
-        if (disposition && disposition.includes('filename=')) {
-          const filenameRegex = /filename=["']?([^"';\n]*)["']?/i;
-          const matches = disposition.match(filenameRegex);
-          if (matches && matches[1]) {
-            filename = decodeURIComponent(matches[1]);
+        let filename;
+        if (type === 'single') {
+          filename = fileList[0].fileNm;
+        } else if (type === 'all') {
+          const contentDisposition = response.headers['content-disposition'];
+          if (contentDisposition) {
+            const filenameRegex = /file[Nn]ame[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
+            const matches = filenameRegex.exec(contentDisposition);
+
+            if (matches != null && matches[1]) {
+              let extractedName = matches[1].replace(/['"]/g, '');
+
+              try {
+                filename = decodeURIComponent(extractedName.replace(/\+/g, ' '));
+              } catch (e) {
+                console.warn('파일명 디코딩 실패:', e);
+                filename = extractedName.replace(/\+/g, ' ');
+              }
+            }
           }
         }
 
-        // 파일 다운로드 처리
-        const blob = new Blob([response.data]);
-        const downloadUrl = window.URL.createObjectURL(blob);
-        const downloadLink = document.createElement('a');
-        downloadLink.href = downloadUrl;
-        downloadLink.setAttribute('download', filename);
-        document.body.appendChild(downloadLink);
-        downloadLink.click();
+        // 파일 다운로드 생성
+        const url = window.URL.createObjectURL(new Blob([response.data]));
+        const link = document.createElement('a');
+        link.href = url;
+        link.setAttribute('download', filename);
+        document.body.appendChild(link);
+        link.click();
+
+        uploadProgressStore.closeModal();
       } catch (error) {
         alert('파일 다운로드 중 오류가 발생했습니다.');
+        uploadProgressStore.closeModal();
       } finally {
-        // Hide loading spinner and clean up
+        // 리소스 정리
         setTimeout(() => {
-          this.loading = false; // Hide loading spinner
-
           if (url) {
             window.URL.revokeObjectURL(url);
           }
client/views/pages/bbsDcryPhoto/PicHistoryInsert.vue
--- client/views/pages/bbsDcryPhoto/PicHistoryInsert.vue
+++ client/views/pages/bbsDcryPhoto/PicHistoryInsert.vue
@@ -115,27 +115,24 @@
       <p>잠시만 기다려주세요</p>
     </div>
   </div>
+  <file-upload-progress />
 </template>
 <script>
-import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
 // COMPONENT
 import PrdctnSelectComponent from '@/views/component/PrdctnSelectComponent.vue';
 import EditorComponent from '@/views/component/editor/EditorComponent.vue';
 import CategoryListComponent from '@/views/component/CategoryListComponent.vue';
+import FileUploadProgress from '@/views/component/FileUploadProgress.vue';
 // API
-import { findDcryProc, saveDcry, updateDcry } from '@/resources/api/dcry';
+import { findDcryProc, saveDcryProc, updateDcryProc } from '@/resources/api/dcry';
 
 export default {
   components: {
-    DoubleLeftOutlined,
-    LeftOutlined,
-    RightOutlined,
-    DoubleRightOutlined,
-    PrdctnSelectComponent,
     // COMPONENT
     PrdctnSelectComponent,
     EditorComponent,
     CategoryListComponent,
+    FileUploadProgress,
   },
 
   data() {
@@ -441,7 +438,7 @@
         }
 
         // API 통신
-        const response = this.$isEmpty(this.pageId) ? await saveDcry(formData) : await updateDcry(formData);
+        const response = this.$isEmpty(this.pageId) ? await saveDcryProc(formData) : await updateDcryProc(formData);
         let id = response.data.data.dcryId;
         alert(this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다.");
 
client/views/pages/bbsDcryVideo/VideoHistoryDetail.vue
--- client/views/pages/bbsDcryVideo/VideoHistoryDetail.vue
+++ client/views/pages/bbsDcryVideo/VideoHistoryDetail.vue
@@ -62,10 +62,6 @@
       <button v-if="isRegister" type="button" class="blue-line" @click="fnMoveTo('edit', pageId)">수정</button>
       <button type="button" class="gray-line-bg" @click="fnMoveTo('list')">목록</button>
       <button type="button" class="gradient" @click="fnDownload">다운로드</button>
-      <div v-if="loading" class="loading-overlay">
-        <div class="loading-spinner"></div>
-        <p>파일을 다운로드 중입니다...</p>
-      </div>
     </div>
   </div>
 </template>
@@ -74,6 +70,7 @@
 import VideoComponent from '@/views/component/player/VideoComponent.vue';
 import ViewerComponent from '@/views/component/editor/ViewerComponent.vue';
 // API
+import uploadProgressStore from '@/resources/js/uploadProgressStore';
 import { findDcryProc, deleteDcryProc } from '@/resources/api/dcry';
 import { fileDownloadProc } from '@/resources/api/file';
 
@@ -97,7 +94,6 @@
       pageId: null,
       findResult: {},
       selectedFiles: [],
-      loading: false,
 
       isRegister: false,
     };
@@ -142,30 +138,45 @@
     async fnDownload() {
       let url = null;
       let link = null;
-      this.loading = true; // Show loading spinner
 
       try {
-        // 파일 ID 수집
-        let file = this.findResult.files[0];
+        const file = this.findResult.files[0];
         const response = await fileDownloadProc(file);
 
-        // 파일명 조회
-        let filename = 'downloadFile.bin';
-        const filenameRegex = /file[Nn]ame[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
-        const matches = filenameRegex.exec(response.headers['content-disposition']);
-        if (matches != null && matches[1]) {
-          filename = matches[1].replace(/['"]/g, '');
+        let filename;
+        if (file.fileNm) {
+          filename = file.fileNm;
+        } else {
+          const contentDisposition = response.headers['content-disposition'];
+          if (contentDisposition) {
+            const filenameRegex = /file[Nn]ame[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
+            const matches = filenameRegex.exec(contentDisposition);
+
+            if (matches != null && matches[1]) {
+              let extractedName = matches[1].replace(/['"]/g, '');
+
+              try {
+                filename = decodeURIComponent(extractedName.replace(/\+/g, ' '));
+              } catch (e) {
+                console.warn('파일명 디코딩 실패:', e);
+                filename = extractedName.replace(/\+/g, ' ');
+              }
+            }
+          }
         }
 
         // 파일 다운로드 생성
-        url = window.URL.createObjectURL(new Blob([response.data]));
-        link = document.createElement('a');
+        const url = window.URL.createObjectURL(new Blob([response.data]));
+        const link = document.createElement('a');
         link.href = url;
         link.setAttribute('download', filename);
         document.body.appendChild(link);
         link.click();
+
+        uploadProgressStore.closeModal();
       } catch (error) {
         alert('파일 다운로드 중 오류가 발생했습니다.');
+        uploadProgressStore.closeModal();
       } finally {
         // 리소스 정리
         setTimeout(() => {
@@ -175,7 +186,6 @@
           if (link && link.parentNode) {
             document.body.removeChild(link);
           }
-          this.loading = false;  // Hide loading spinner
         }, 100);
       }
     },
client/views/pages/bbsDcryVideo/VideoHistoryInsert.vue
--- client/views/pages/bbsDcryVideo/VideoHistoryInsert.vue
+++ client/views/pages/bbsDcryVideo/VideoHistoryInsert.vue
@@ -61,13 +61,16 @@
               <div id="fileNames" class="file-names">
                 <div v-if="requestDTO.files.length === 0 && multipartFiles.length === 0">선택된 파일이 없습니다.</div>
                 <!-- 기존 등록된 파일 목록 -->
-                <div v-for="(file, idx) of requestDTO.files" :key="idx" class="flex-sp-bw mb-5 file-wrap">
-                  <div class="file-name">
-                    <img src="/client/resources/images/icon/imgicon.png" alt="fileicon">
-                    <p>{{ file.fileNm }}</p>
+                <template v-for="(file, idx) of requestDTO.files" :key="idx">
+                  <div v-if="file.fileOrdr == 1" class="flex-sp-bw mb-5 file-wrap">
+                    <div class="file-name">
+                      <img src="/client/resources/images/icon/imgicon.png" alt="fileicon">
+                      <p>{{ file.fileNm }}</p>
+                    </div>
+                    <button type="button" class="cancel" @click="fnDelFile('old', file.fileOrdr)"><b>✕</b></button>
                   </div>
-                  <button type="button" class="cancel" @click="fnDelFile('old', file.fileOrdr)"><b>✕</b></button>
-                </div><!-- 새로 추가된 파일 목록 -->
+                </template>
+                <!-- 새로 추가된 파일 목록 -->
                 <div v-for="(file, idx) of multipartFiles" :key="idx" class="flex-sp-bw mb-5 file-wrap">
                   <div class="file-name">
                     <img src="/client/resources/images/icon/imgicon.png" alt="fileicon">
@@ -96,26 +99,24 @@
       <p>잠시만 기다려주세요</p>
     </div>
   </div>
+  <file-upload-progress />
 </template>
 <script>
-import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
 // COMPONENT
 import PrdctnSelectComponent from '@/views/component/PrdctnSelectComponent.vue';
 import EditorComponent from '@/views/component/editor/EditorComponent.vue';
 import CategoryListComponent from '@/views/component/CategoryListComponent.vue';
+import FileUploadProgress from '@/views/component/FileUploadProgress.vue';
 // API
-import { findDcryProc, saveDcry, updateDcry } from '@/resources/api/dcry';
+import uploadProgressStore from '@/resources/js/uploadProgressStore';
+import { findDcryProc, saveDcryProc, updateDcryProc } from '@/resources/api/dcry';
 
 export default {
   components: {
-    DoubleLeftOutlined,
-    LeftOutlined,
-    RightOutlined,
-    DoubleRightOutlined,
-    // COMPONENT
     PrdctnSelectComponent,
     EditorComponent,
     CategoryListComponent,
+    FileUploadProgress,
   },
 
   data() {
@@ -166,6 +167,9 @@
     if (!this.$isEmpty(this.pageId)) {
       this.fnFindDcry(); // 상세 조회
     }
+
+    // 업로드 상태 초기화 - 모달이 떠있는 문제 해결
+    uploadProgressStore.resetState();
   },
 
   methods: {
@@ -229,7 +233,7 @@
 
     // 파일 업로드 처리 함수
     processFiles(files) {
-      const allowedTypes = ['mp4', 'mov', 'avi', 'wmv', 'mkv', 'webm', 'mxf']; // 'mxf' 추가
+      const allowedTypes = ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm', 'mxf'];
       const maxSize = 10 * 1024 * 1024 * 1024; // 10GB
 
       // 유효성 검사
@@ -244,7 +248,7 @@
 
         // 파일 타입 검증
         if (!allowedTypes.includes(fileType)) {
-          alert(`${file.name} 파일은 허용되지 않는 형식입니다. 영상 파일(mp4, mov, avi, wmv, mkv, webm, mxf)만 업로드 가능합니다.`);
+          alert(`${file.name} 파일은 허용되지 않는 형식입니다. 영상 파일(mp4, avi, mov, wmv, flv, mkv, webm, mxf)만 업로드 가능합니다.`);
           this.resetFileInput(); // input 파일 목록 비움
           return;
         }
@@ -277,7 +281,7 @@
           this.resetFileInput(); // input 파일 목록 비움
         }
       } else if (type === 'old') {
-        this.requestDTO.files = this.requestDTO.files.filter(item => item.fileOrdr !== separator);
+        this.requestDTO.files = [];
       }
     },
 
@@ -365,7 +369,7 @@
         }
 
         // API 통신
-        const response = this.$isEmpty(this.pageId) ? await saveDcry(formData) : await updateDcry(formData);
+        const response = this.$isEmpty(this.pageId) ? await saveDcryProc(formData) : await updateDcryProc(formData);
         let id = response.data.data.dcryId;
         alert(this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다.");
 
client/views/pages/bbsNesDta/NewsReleaseInsert.vue
--- client/views/pages/bbsNesDta/NewsReleaseInsert.vue
+++ client/views/pages/bbsNesDta/NewsReleaseInsert.vue
@@ -61,14 +61,6 @@
               <p class="mb-10">파일목록</p>
               <div id="fileNames" class="file-names">
                 <div v-if="requestDTO.files.length === 0 && multipartFiles.length === 0">선택된 파일이 없습니다.</div>
-                <!-- 새로 추가된 파일 목록 -->
-                <div v-for="(file, idx) of multipartFiles" :key="idx" class="flex-sp-bw mb-5 file-wrap">
-                  <div class="file-name">
-                    <img src="/client/resources/images/icon/imgicon.png" alt="fileicon">
-                    <p>{{ file.name }}</p>
-                  </div>
-                  <button type="button" class="cancel" @click="fnDelFile('new', idx)"><b>✕</b></button>
-                </div>
                 <!-- 기존 등록된 파일 목록 -->
                 <div v-for="(file, idx) of requestDTO.files" :key="idx" class="flex-sp-bw mb-5 file-wrap">
                   <div class="file-name">
@@ -76,6 +68,14 @@
                     <p>{{ file.fileNm }}</p>
                   </div>
                   <button type="button" class="cancel" @click="fnDelFile('old', file.fileId)"><b>✕</b></button>
+                </div>
+                <!-- 새로 추가된 파일 목록 -->
+                <div v-for="(file, idx) of multipartFiles" :key="idx" class="flex-sp-bw mb-5 file-wrap">
+                  <div class="file-name">
+                    <img src="/client/resources/images/icon/imgicon.png" alt="fileicon">
+                    <p>{{ file.name }}</p>
+                  </div>
+                  <button type="button" class="cancel" @click="fnDelFile('new', idx)"><b>✕</b></button>
                 </div>
               </div>
             </li>
@@ -91,26 +91,23 @@
       </button>
     </div>
   </div>
+  <file-upload-progress />
 </template>
 <script>
-import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
 // COMPONENT
 import PrdctnSelectComponent from '@/views/component/PrdctnSelectComponent.vue';
 import EditorComponent from '@/views/component/editor/EditorComponent.vue';
 import CategoryListComponent from '@/views/component/CategoryListComponent.vue';
+import FileUploadProgress from '@/views/component/FileUploadProgress.vue';
 // API
 import { findNesDtaProc, saveNesDtaProc, updateNesDtaProc } from '@/resources/api/nesDta';
 
 export default {
   components: {
-    DoubleLeftOutlined,
-    LeftOutlined,
-    RightOutlined,
-    DoubleRightOutlined,
-    // COMPONENT
     PrdctnSelectComponent,
     EditorComponent,
     CategoryListComponent,
+    FileUploadProgress,
   },
 
   data() {
@@ -295,7 +292,6 @@
       try {
         const formData = new FormData();
 
-        // 텍스트 데이터 추가
         formData.append('sj', this.requestDTO.sj);
 
         if (!this.$isEmpty(this.pageId)) {
@@ -317,32 +313,13 @@
           }
         }
 
-        // 파일 아이디
         if (!this.$isEmpty(this.requestDTO.fileId)) {
           formData.append('fileId', this.requestDTO.fileId);
         }
 
-        // 카테고리 Ids 추가
         if (this.selectedCtgries && this.selectedCtgries.length > 0) {
           for (let ctgry of this.selectedCtgries) {
             formData.append("ctgryIds", ctgry.ctgryId);
-          }
-        }
-
-        // 파일 추가
-        if (this.multipartFiles.length > 0) {
-          formData.append("multipartFiles", this.multipartFiles[0]);
-
-          // 썸네일 정보 추가
-          if (this.selectedThumb !== null) {
-            formData.append('selectedThumb', this.multipartFiles[0].name);
-          }
-        }
-
-        // 기존파일 수정
-        if (!this.$isEmpty(this.pageId) && this.requestDTO.files.length > 0) {
-          for (let file of this.requestDTO.files) {
-            formData.append("files", file.fileOrdr);
           }
         }
 
Add a comment
List