jichoi / calendar star
하관우 하관우 07-31
2025-07-31 하관우 파일명 수정
@d045b6d80ecef5e1a473d27a38ac4b29018be721
client/resources/api/user.js
--- client/resources/api/user.js
+++ client/resources/api/user.js
@@ -1,5 +1,4 @@
 import { apiClient, fileClient } from "./index";
-import uploadService from "@/resources/js/uploadService";
 
 // 조회 - 목록
 export const findUsersProc = data => {
 
client/resources/js/downloadService.js (deleted)
--- client/resources/js/downloadService.js
@@ -1,257 +0,0 @@
-// @/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 downloadExcel(searchReqDTO, fileName, options = {}) {
-    try {
-      // 다운로드 상태 초기화 (현재 진행률 유지)
-      const currentProgress = uploadProgressStore.totalProgress;
-      uploadProgressStore.startDownload(fileName || '파일 다운로드 중...');
-      uploadProgressStore.setStage('downloading');
-
-      // 이전 진행률이 있으면 유지
-      if (currentProgress > 0) {
-        uploadProgressStore.totalProgress = currentProgress;
-      }
-
-      // 즉시 진행률 시뮬레이션 시작 (서버 응답 전)
-      this._startProgressSimulation();
-
-      // apiClient를 사용하여 다운로드 요청
-      const response = await apiClient.get(`/excel/excelDownloadAll.json`, {
-        params: searchReqDTO, // 검색 조건 파라미터 전달
-        responseType: 'blob', // 엑셀 파일을 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 (deleted)
--- client/resources/js/uploadProgressStore.js
@@ -1,133 +0,0 @@
-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 (deleted)
--- client/resources/js/uploadService.js
@@ -1,109 +0,0 @@
-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/component/FileUploadProgress.vue (deleted)
--- client/views/component/FileUploadProgress.vue
@@ -1,267 +0,0 @@
-// @/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/Popup/HrPopup.vue
--- client/views/component/Popup/HrPopup.vue
+++ client/views/component/Popup/HrPopup.vue
@@ -7,7 +7,8 @@
           <div class="sch-form-wrap">
             <div class="input-group">
               <div class="sch-input">
-                <input type="text" class="form-control" placeholder="직원명" v-model="request.searchText" @keyup.enter="findDatas">
+                <input type="text" class="form-control" placeholder="직원명" v-model="request.searchText"
+                  @keyup.enter="findDatas">
                 <button type="button" class="ico-sch" @click="findDatas">
                   <SearchOutlined />
                 </button>
@@ -32,7 +33,8 @@
                   <td>{{ item.deptNm }}</td>
                   <td>{{ item.userNm }}</td>
                   <td>
-                    <button type="button" class="btn sm sm secondary" @click="selectPerson(item)" :disabled="isUserSelected(item.userId)">선택</button>
+                    <button type="button" class="btn sm sm secondary" @click="selectPerson(item)"
+                      :disabled="isUserSelected(item.userId)">선택</button>
                   </td>
                 </tr>
               </tbody>
@@ -81,7 +83,7 @@
 
   computed: {
     selectedUserIds() {
-      return new Set(this.lists.map(sanctn => sanctn.confmerId));
+      return new Set(this.lists.map(member => member.userId)); // userId로 변경
     }
   },
 
@@ -121,6 +123,7 @@
     // 승인자 선택
     selectPerson(item) {
       this.$emit('onSelected', item);
+      this.$emit('close');
     },
 
     // 페이지 이동
client/views/layout/LeftNavBar.vue
--- client/views/layout/LeftNavBar.vue
+++ client/views/layout/LeftNavBar.vue
@@ -149,22 +149,22 @@
       }
     ]
   },
-  hr: {
-    path: '/hr-management',
+  hrm: {
+    path: '/hrm-management',
     menus: [
       {
-        name: 'hr',
+        name: 'hrm',
         title: '직원',
         items: [
-          { name: 'hrSearch', title: '직원검색' },
-          { name: 'hrManagement', title: '직원관리' }
+          { name: 'HrmSearch', title: '직원검색' },
+          { name: 'HrmManagement', title: '직원관리' }
         ]
       },
       {
-        name: 'dept',
+        name: 'team',
         title: '부서',
         items: [
-          { name: 'buseoManagement', title: '부서관리' }
+          { name: 'TeamManagement', title: '부서관리' }
         ]
       }
     ]
@@ -220,7 +220,7 @@
         expense: () => ['cost', 'meeting'].some(name => this.currentRouteName.includes(name)),
         vhcle: () => this.currentRouteName.includes('vhcle'),
         card: () => this.currentRouteName.includes('card'),
-        hr: () => this.currentRouteName.includes('hr'),
+        hrm: () => this.currentRouteName.includes('hrm'),
         dept: () => this.currentRouteName.includes('buseo')
       }
 
client/views/layout/Menu.vue
--- client/views/layout/Menu.vue
+++ client/views/layout/Menu.vue
@@ -21,7 +21,7 @@
     </li>
     <i class="fas fa-bars"></i>
     <li class="nav-item">
-      <router-link to="/hr-management" class="nav-link " active-class="active"><span>인사관리</span></router-link>
+      <router-link to="/Hrm-management" class="nav-link " active-class="active"><span>인사관리</span></router-link>
     </li>
     <i class="fas fa-bars"></i>
     <li class="nav-item">
client/views/pages/AppRouter.js
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
@@ -46,11 +46,11 @@
 import CardInfoManagement from '../pages/Manager/asset/CardInfoManagement.vue';
 
 //인사관리
-import hrSearch from '../pages/Manager/hr/hrSearch.vue';
-import hrManagement from '../pages/Manager/hr/hrManagement.vue';
-import hrDetail from '../pages/Manager/hr/hrDetail.vue';
-import hrInsert from '../pages/Manager/hr/hrInsert.vue';
-import buseoManagement from '../pages/Manager/hr/buseoManagement.vue';
+import HrmSearch from '../pages/Manager/hrm/HrmSearch.vue';
+import HrmManagement from '../pages/Manager/hrm/HrmManagement.vue';
+import HrmDetail from '../pages/Manager/hrm/HrmDetail.vue';
+import HrmInsert from '../pages/Manager/hrm/HrmInsert.vue';
+import TeamManagement from '../pages/Manager/hrm/TeamManagement.vue';
 
 //시스템관리
 import AuthorManagementComp from '../pages/Manager/system/AuthorManagement.vue';
@@ -127,13 +127,13 @@
   },
   // 인사관리
   {
-    path: '/hr-management', name: 'hr', redirect: '/hr-management.page/hrSearch.page',
+    path: '/hrm-management', name: 'hrm', redirect: '/Hrm-management.page/HrmSearch.page',
     children: [
-      { path: 'hrSearch.page', name: 'hrSearch', component: hrSearch },
-      { path: 'hrManagement.page', name: 'hrManagement', component: hrManagement },
-      { path: 'hrDetail.page', name: 'hrDetail', component: hrDetail },
-      { path: 'hrInsert.page', name: 'hrInsert', component: hrInsert },
-      { path: 'buseoManagement.page', name: 'buseoManagement', component: buseoManagement },
+      { path: 'HrmSearch.page', name: 'HrmSearch', component: HrmSearch },
+      { path: 'HrmManagement.page', name: 'HrmManagement', component: HrmManagement },
+      { path: 'HrmDetail.page', name: 'HrmDetail', component: HrmDetail },
+      { path: 'HrmInsert.page', name: 'HrmInsert', component: HrmInsert },
+      { path: 'TeamManagement.page', name: 'TeamManagement', component: TeamManagement },
     ]
   },
   // 시스템관리
 
client/views/pages/Manager/hr/buseoManagement.vue (deleted)
--- client/views/pages/Manager/hr/buseoManagement.vue
@@ -1,331 +0,0 @@
-<template>
-  <div class="card">
-    <div class="card-body">
-      <h2 class="card-title">부서 관리</h2>
-      <!-- Multi Columns Form -->
-      <div class="flex align-top">
-        <div class="sch-form-wrap search  ">
-          <div v-for="(menu, index) in menus" :key="index" class="sidemenu">
-            <details class="menu-box" open>
-              <summary class="topmenu">
-                <img :src="arrow" alt="" class="arrow">
-                <img :src="topmenuicon" alt="">
-                <p>{{ menu.title }} </p>
-                <button @click="addSubMenu(index)" class="btn sm xsm secondary">sub  +</button>
-              </summary>
-              <ul>
-                <li class="submenu" v-for="(submenu, subIndex) in menu.submenus" :key="subIndex">
-                  <router-link :to="submenu.link" exact-active-class="active-link" v-slot="{ isExactActive }">
-                    <img :src="menuicon" alt="">
-                    <p>{{ submenu.label }}</p>
-                  </router-link>
-                </li>
-              </ul>
-            </details>
-          </div>
-          <div class="buttons">
-            <button @click="addTopMenu"><img :src="addtopmenu" alt=""></button>
-            
-          </div>
-        </div>
-        <div style="width: 100%;">
-          <div class=" sch-form-wrap title-wrap">
-            <h3><img :src="h3icon" alt="">부서 정보</h3>
-            <div class="buttons" style="margin: 0;">
-              <button type="submit" class="btn sm sm tertiary">초기화</button>
-              <button type="reset" class="btn sm sm secondary">등록</button>
-              <button type="delete" class="btn sm sm btn-red">삭제</button>
-            </div>
-          </div>
-          <form class="row g-3 pt-3  needs-validation detail" @submit.prevent="handleSubmit"
-            style="margin-bottom: 3rem;">
-            <div class="col-12">
-              <label for="purpose" class="form-label">상위부서</label>
-              <input type="text" class="form-control" id="purpose" v-model="purpose" readonly />
-            </div>
-            <div class="col-12">
-              <label for="purpose" class="form-label">
-                <p>부서명
-                <p class="require"><img :src="require" alt=""></p>
-                </p>
-              </label>
-              <input type="text" class="form-control" id="purpose" v-model="purpose" />
-            </div>
-
-            <div class="col-12 chuljang border-x">
-              <label for="prvonsh" class="form-label">부서설명</label>
-              <input type="text" class="form-control textarea" id="reason" v-model="reason" />
-            </div>
-
-
-          </form>
-          <div class=" sch-form-wrap title-wrap">
-            <h3><img :src="h3icon" alt="">부서 사용자</h3>
-            <div class="buttons" style="margin: 0;">
-              <button type="reset" class="btn sm sm secondary" @click="showPopup = true">사용자 추가</button>
-              <button type="delete" class="btn sm sm btn-red" @click="removeMember(index)">사용자 삭제</button>
-            </div>
-            <HrPopup v-if="showPopup" @close="showPopup = false" @select="addMember"/>
-          </div>
-          <div class="tbl-wrap chk-area">
-            <table id="myTable" class="tbl data">
-
-              <thead>
-                <tr>
-                  <th>선택</th>
-                  <th>직급</th>
-                  <th>이름</th>
-                  <th>부서장</th>
-                </tr>
-              </thead>
-              <!-- 동적으로 <td> 생성 -->
-              <tbody>
-                <tr v-for="(member, index) in members" :key="index">
-                  <td>
-                    <div class="form-check">
-                      <input type="checkbox" :id="`chk_${index}`" :value="member.name" v-model="member.checked" />
-                      <label :for="`chk_${index}`"></label>
-                    </div>
-                  </td>
-                  <td>{{ member.position }}</td>
-                  <td>{{ member.name }}</td>
-                  <td>
-                    <div class="form-check">
-                      <input type="radio" name="manager" :id="`rdo_${index}`" :value="member.name"
-                        v-model="selectedManager" />
-                      <label :for="`rdo_${index}`"></label>
-                    </div>
-                  </td>
-                </tr>
-              </tbody>
-            </table>
-
-          </div>
-          <div class="pagination">
-            <ul>
-              <!-- 왼쪽 화살표 (이전 페이지) -->
-              <li class="arrow" :class="{ disabled: currentPage === 1 }" @click="changePage(currentPage - 1)">
-                &lt;
-              </li>
-
-              <!-- 페이지 번호 -->
-              <li v-for="page in totalPages" :key="page" :class="{ active: currentPage === page }"
-                @click="changePage(page)">
-                {{ page }}
-              </li>
-
-              <!-- 오른쪽 화살표 (다음 페이지) -->
-              <li class="arrow" :class="{ disabled: currentPage === totalPages }" @click="changePage(currentPage + 1)">
-                &gt;
-              </li>
-            </ul>
-          </div>
-        </div>
-      </div>
-
-
-    </div>
-  </div>
-</template>
-
-<script>
-import { ref } from 'vue';
-import { PlusCircleFilled, CloseOutlined, DownOutlined } from '@ant-design/icons-vue';
-import HrPopup from '../../../component/Popup/HrPopup.vue';
-const isOpen = ref(false)
-export default {
-  data() {
-    const today = new Date().toISOString().split('T')[0];
-    return {
-      selectedManager: '',
-      showPopup: false,
-      menus: [
-        {
-          title: '부서1',
-          submenus: [
-            {
-              label: '직원검색',
-              link: { name: 'hrSearch' },
-            },
-          ],
-        },
-      ],
-      currentPage: 1,
-      totalPages: 3,
-      members: [] ,
-      selectedManager: null,
-      h3icon: "/client/resources/img/h3icon.png",
-      require: "/client/resources/img/require.png",
-      menuicon: "/client/resources/img/arrow-rg.png",
-      topmenuicon: "/client/resources/img/topmenu.png",
-      arrow: "/client/resources/img/arrow.png",
-      addtopmenu: "/client/resources/img/addtopmenu.png",
-      addsubmenu: "/client/resources/img/addsubmenu.png",
-      fileName: '',
-      startDate: today,
-      startTime: '09:00',
-      endDate: today,
-      endTime: '18:00',
-      where: '',
-      purpose: '',
-      approvals: [
-        {
-          category: '결재',
-          name: '',
-        },
-      ],
-      receipts: [
-        {
-          type: '개인결제',
-          category: '결재',
-          category1: '구분',
-        },
-      ],
-    };
-  },
-  components: {
-    PlusCircleFilled, CloseOutlined, DownOutlined, HrPopup
-  },
-  computed: {
-    loginUser() {
-      const authStore = useAuthStore();
-      return authStore.getLoginUser;
-    },
-  },
-
-  methods: {
-    addMember(selectedUser) {
-      this.members.push({
-        position: selectedUser.position,
-        name: selectedUser.name, // or other fields if needed
-      });
-      this.showPopup = false; // 팝업 닫기
-    },
-    removeMember() {
-      this.members = this.members.filter(member => !member.checked);
-    },
-    addTopMenu() {
-      const newIndex = this.menus.length + 1;
-      this.menus.push({
-        title: `부서${newIndex}`,
-        submenus: [],
-      });
-    },
-    addSubMenu(menuIndex) {
-      this.menus[menuIndex].submenus.push({
-        label: `신규메뉴${this.menus[menuIndex].submenus.length + 1}`,
-        link: { name: 'hrSearch' },
-      });
-    },
-    goToPage(type) {
-      if (type === '회의록 등록') {
-        this.$router.push({ name: 'meetingInsert' });
-      } else if (type === '출장') {
-        this.$router.push({ name: 'ChuljangDetail' });
-      }
-    },
-    handleFileUpload(event) {
-      const file = event.target.files[0];
-      if (file) {
-        this.fileName = file.name;
-      }
-    },
-    addApproval() {
-      this.approvals.push({
-        category: '결재',
-        name: '',
-      });
-    },
-    addReceipt() {
-      this.receipts.push({
-        type: '개인결제',
-        category: '결재',
-        category1: '',
-        name: '',
-        file: null,
-      });
-    },
-    // 승인자 삭제
-    removeApproval(index) {
-      this.approvals.splice(index, 1);
-    },
-    removeReceipt(index) {
-      this.receipts.splice(index, 1);
-    },
-    validateForm() {
-      // 필수 입력 필드 체크
-      if (
-        this.startDate &&
-        this.startTime &&
-        this.endDate &&
-        this.endTime &&
-        this.where &&
-        this.purpose.trim() !== ""
-      ) {
-        this.isFormValid = true;
-      } else {
-        this.isFormValid = false;
-      }
-    },
-    calculateDayCount() {
-      const start = new Date(`${this.startDate}T${this.startTime}:00`);
-      const end = new Date(`${this.endDate}T${this.endTime}:00`);
-
-      let totalHours = (end - start) / (1000 * 60 * 60);  // 밀리초를 시간 단위로 변환
-
-      if (this.startDate !== this.endDate) {
-        // 시작일과 종료일이 다른경우
-        const startDateObj = new Date(this.startDate);
-        const endDateObj = new Date(this.endDate);
-        const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산
-        if (this.startTime !== "09:00" || this.endTime !== "18:00") {
-          this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우
-        } else {
-          this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우
-        }
-      } else {
-        // 시작일과 종료일이 같은 경우
-        if (this.startTime !== "09:00" || this.endTime !== "18:00") {
-          this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5
-        } else {
-          this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주
-        }
-      }
-
-      this.validateForm(); // dayCount 변경 후 폼 재검증
-    },
-    handleSubmit() {
-      this.validateForm(); // 제출 시 유효성 확인
-      if (this.isFormValid) {
-        localStorage.setItem('ChuljangFormData', JSON.stringify(this.$data));
-        alert("승인 요청이 완료되었습니다.");
-        // 추가 처리 로직 (API 요청 등)
-      } else {
-        alert("모든 필드를 올바르게 작성해주세요.");
-      }
-    },
-    loadFormData() {
-      const savedData = localStorage.getItem('ChuljangFormData');
-      if (savedData) {
-        this.$data = JSON.parse(savedData);
-      }
-    },
-  },
-  mounted() {
-    // Load the saved form data when the page is loaded
-    this.loadFormData();
-  },
-  watch: {
-    startDate: 'calculateDayCount',
-    startTime: 'calculateDayCount',
-    endDate: 'calculateDayCount',
-    endTime: 'calculateDayCount',
-    where: 'validateForm',
-    purpose: "validateForm",
-  },
-};
-</script>
-
-<style scoped>
-/* 필요한 스타일 추가 */
-</style>
 
client/views/pages/Manager/hr/hr.vue (deleted)
--- client/views/pages/Manager/hr/hr.vue
@@ -1,99 +0,0 @@
-<template>
-  <div class="sidemenu">
-    <div class="myinfo simple">
-      <div class="name-box">
-        <div class="img-area">
-          <div><img :src="photoicon" alt="">
-            <p class="name">OOO과장</p>
-          </div>
-          <div class="info">
-            <p>솔루션 개발팀</p>
-            <i class="fa-bars"></i>
-            <p>팀장</p>
-          </div>
-        </div>
-      </div>
-
-
-      <details class="menu-box" open>
-          <summary><p>직원</p><div class="icon"><img :src="topmenuicon" alt=""></div></summary>
-          <ul>
-            <li> <router-link :to="{ name: 'hrSearch' }" exact-active-class="active-link" v-slot="{ isExactActive }">
-                <p>직원검색</p>
-                <div class="icon" v-if="isExactActive">
-                  <img :src="menuicon" alt="">
-                </div>
-              </router-link></li>
-            <li>
-              <router-link :to="{ name: 'hrManagement' }" exact-active-class="active-link" v-slot="{ isExactActive }">
-                <p>직원관리</p>
-                <div class="icon" v-if="isExactActive">
-                  <img :src="menuicon" alt="">
-                </div>
-              </router-link>
-            </li>
-
-
-          </ul>
-      </details>
-      <details class="menu-box">
-          <summary><p>부서</p><div class="icon"><img :src="topmenuicon" alt=""></div></summary>
-          <ul>
-            <li> <router-link :to="{ name: 'buseoManagement' }" exact-active-class="active-link" v-slot="{ isExactActive }">
-                <p>부서관리</p>
-                <div class="icon" v-if="isExactActive">
-                  <img :src="menuicon" alt="">
-                </div>
-              </router-link></li>
-
-
-          </ul>
-      </details>
-    </div>
-  </div>
-  <!-- End Page Title -->
-  <div class="content">
-    <router-view></router-view>
-
-  </div>
-</template>
-
-<script>
-import { ref } from 'vue';
-
-export default {
-  data() {
-    return {
-      photoicon: "/client/resources/img/photo_icon.png",
-      menuicon: "/client/resources/img/menuicon.png",
-      topmenuicon: "/client/resources/img/topmenuicon.png",
-      // 데이터 초기화
-      years: [2023, 2024, 2025], // 연도 목록
-      months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록
-      selectedYear: '',
-      selectedMonth: '',
-      DeptData: [
-        { member: '', deptNM: '', acceptTerms: false },
-        // 더 많은 데이터 추가...
-      ],
-      filteredData: [],
-    };
-  },
-  computed: {
-  },
-  methods: {
-
-    // 페이지 변경
-    changePage(page) {
-      this.currentPage = page;
-    },
-  },
-  created() {
-  },
-  mounted() {
-
-  },
-};
-</script>
-
-<style scoped></style>
client/views/pages/Manager/hrm/HrmDetail.vue (Renamed from client/views/pages/Manager/hr/hrDetail.vue)
--- client/views/pages/Manager/hr/hrDetail.vue
+++ client/views/pages/Manager/hrm/HrmDetail.vue
@@ -118,7 +118,7 @@
       }
     },
     goToPage() {
-      this.$router.push({ name: 'hrManagement' });
+      this.$router.push({ name: 'HrmManagement' });
     },
     goToDetail() {
       this.$router.push({
@@ -167,7 +167,7 @@
       const confirmDelete = confirm("정말로 사용자를 복구시키겠습니까?\n이 작업은 되돌릴 수 없습니다.");
 
       if (!confirmDelete) { // 사용자가 '아니오'를 눌렀을 때
-        alert("사용자 복구구가 취소되었습니다.");
+        alert("사용자 복구가 취소되었습니다.");
         return; // 함수 실행 중단
       }
 
client/views/pages/Manager/hrm/HrmInsert.vue (Renamed from client/views/pages/Manager/hr/hrInsert.vue)
--- client/views/pages/Manager/hr/hrInsert.vue
+++ client/views/pages/Manager/hrm/HrmInsert.vue
@@ -149,7 +149,6 @@
 import BuseoPopup from "../../../component/Popup/BuseoPopup.vue";
 import { findAuthorsProc } from "../../../../resources/api/author";
 import ImageCropper from 'vue-image-crop-upload'; //이미지 자르기기
-import FileUploadProgress from '@/views/component/FileUploadProgress.vue';
 import { joinProc, findUsersDetProc, updateUsersProc } from "../../../../resources/api/user";
 import { findCodesProc } from "../../../../resources/api/code";
 
@@ -225,7 +224,7 @@
     }
   },
   components: {
-    SearchOutlined, PlusCircleFilled, BuseoPopup, CloseOutlined, findAuthorsProc, ImageCropper, FileUploadProgress, joinProc, findCodesProc
+    SearchOutlined, PlusCircleFilled, BuseoPopup, CloseOutlined, findAuthorsProc, ImageCropper, joinProc, findCodesProc
   },
   created() {
     if (this.$route.query.id != null) {
@@ -267,7 +266,7 @@
     },
     // 취소 누를 시 직원관리 페이지로 이동
     goToManagementList() {
-      this.$router.push('/hr-management/hrManagement.page');
+      this.$router.push('/hrm-management/HrmManagement.page');
     },
     // 공통코드 직급
     async clsfTypeCodes() {
@@ -370,7 +369,6 @@
 
         if (response.status === 200) {
           alert('사용자 정보가 성공적으로 저장되었습니다!');
-          // 성공 시 페이지 이동 (예: this.$router.push('/hr-management/hrManagement.page'))
         }
       } catch (error) {
         console.error("사용자 정보 저장 중 오류 발생:", error);
@@ -433,7 +431,6 @@
 
         if (response.status === 200) {
           alert('사용자 정보가 성공적으로 저장되었습니다!');
-          // 성공 시 페이지 이동 (예: this.$router.push('/hr-management/hrManagement.page'))
         }
       } catch (error) {
         console.error("사용자 정보 저장 중 오류 발생:", error);
client/views/pages/Manager/hrm/HrmManagement.vue (Renamed from client/views/pages/Manager/hr/hrManagement.vue)
--- client/views/pages/Manager/hr/hrManagement.vue
+++ client/views/pages/Manager/hrm/HrmManagement.vue
@@ -186,11 +186,11 @@
     },
     goToDetailPage(item) {
       // item.id 또는 다른 식별자를 사용하여 URL을 구성할 수 있습니다.
-      this.$router.push({ name: 'hrDetail', query: { id: item.userId } });
+      this.$router.push({ name: 'HrmDetail', query: { id: item.userId } });
     },
     goToPage(type) {
       if (type === '등록') {
-        this.$router.push({ name: 'hrInsert' });
+        this.$router.push({ name: 'HrmInsert' });
       }
     },
   },
client/views/pages/Manager/hrm/HrmSearch.vue (Renamed from client/views/pages/Manager/hr/hrSearch.vue)
--- client/views/pages/Manager/hr/hrSearch.vue
+++ client/views/pages/Manager/hrm/HrmSearch.vue
No changes
 
client/views/pages/Manager/hrm/TeamManagement.vue (added)
+++ client/views/pages/Manager/hrm/TeamManagement.vue
@@ -0,0 +1,644 @@
+<template>
+  <div class="card">
+    <div class="card-body">
+      <h2 class="card-title">부서 관리</h2>
+      <!-- Multi Columns Form -->
+      <div class="flex align-top">
+        <div class="sch-form-wrap search">
+          <div v-for="(menu, index) in menus" :key="index" class="sidemenu">
+            <div class="menu-box">
+              <div class="topmenu" @click="toggleMenu(index)">
+                <img :src="arrow" alt="" class="arrow" :class="{ 'arrow-open': menu.isOpen }">
+                <img :src="topmenuicon" alt="">
+                <p @click.stop="selectTopDepartment(menu.title)" 
+                   :class="{ 
+                     'active-top': selectedDepartment?.name === menu.title && selectedDepartment?.isTopLevel 
+                   }" 
+                   class="top-dept-name">{{ menu.title }}</p>
+                <button @click.stop="addSubMenu(index)" class="btn sm xsm secondary">sub +</button>
+              </div>
+              <ul v-show="menu.isOpen" class="submenu-list">
+                <li class="submenu" v-for="(submenu, subIndex) in menu.submenus" :key="subIndex">
+                  <div @click="selectDepartment(menu.title, submenu.label)" 
+                       :class="{ 
+                         'active-link': selectedDepartment?.parent === menu.title && selectedDepartment?.name === submenu.label && !selectedDepartment?.isTopLevel 
+                       }"
+                       class="submenu-item">
+                    <img :src="menuicon" alt="">
+                    <p>{{ submenu.label }}</p>
+                  </div>
+                </li>
+              </ul>
+            </div>
+          </div>
+          <div class="buttons">
+            <button @click="addTopMenu"><img :src="addtopmenu" alt=""></button>
+          </div>
+        </div>
+        
+        <div style="width: 100%;">
+          <div class="sch-form-wrap title-wrap">
+            <h3><img :src="h3icon" alt="">부서 정보</h3>
+            <div class="buttons" style="margin: 0;">
+              <button type="button" class="btn sm sm tertiary">초기화</button>
+              <button type="button" class="btn sm sm secondary">등록</button>
+              <button type="button" class="btn sm sm btn-red">삭제</button>
+            </div>
+          </div>
+          
+          <form class="row g-3 pt-3 needs-validation detail" @submit.prevent="handleSubmit" style="margin-bottom: 3rem;">
+            <div class="col-12">
+              <label for="parentDept" class="form-label">상위부서</label>
+              <input type="text" class="form-control" id="parentDept" v-model="departmentInfo.parentDept" readonly />
+            </div>
+            
+            <div class="col-12">
+              <label for="deptName" class="form-label">
+                <p>부서명
+                  <span class="require"><img :src="require" alt=""></span>
+                </p>
+              </label>
+              <input type="text" class="form-control" id="deptName" v-model="departmentInfo.deptName" />
+            </div>
+
+            <div class="col-12 chuljang border-x">
+              <label for="deptDesc" class="form-label">부서설명</label>
+              <textarea class="form-control textarea" id="deptDesc" v-model="departmentInfo.deptDesc" rows="3"></textarea>
+            </div>
+          </form>
+          
+          <div class="sch-form-wrap title-wrap">
+            <h3><img :src="h3icon" alt="">부서 사용자</h3>
+            <div class="buttons" style="margin: 0;">
+              <button type="button" class="btn sm sm secondary" @click="showPopup = true">사용자 추가</button>
+              <button type="button" class="btn sm sm btn-red" @click="removeMember()">사용자 삭제</button>
+            </div>
+            <HrPopup v-if="showPopup" 
+                     :lists="members" 
+                     @close="showPopup = false" 
+                     @onSelected="addMember"/>
+          </div>
+          
+          <div class="tbl-wrap chk-area">
+            <table id="myTable" class="tbl data">
+              <thead>
+                <tr>
+                  <th>선택</th>
+                  <th>직급</th>
+                  <th>직책</th>
+                  <th>부서</th>
+                  <th>이름</th>
+                  <th>부서장</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr v-for="(member, index) in members" :key="index">
+                  <td>
+                    <div class="form-check">
+                      <input type="checkbox" :id="`chk_${index}`" :value="member.name" v-model="member.checked" />
+                      <label :for="`chk_${index}`"></label>
+                    </div>
+                  </td>
+                  <td>{{ member.position }}</td>
+                  <td>{{ member.responsibility }}</td>
+                  <td>{{ member.department }}</td>
+                  <td>{{ member.name }}</td>
+                  <td>
+                    <div class="form-check">
+                      <input type="radio" name="manager" :id="`rdo_${index}`" :value="member.userId" v-model="selectedManager" />
+                      <label :for="`rdo_${index}`"></label>
+                    </div>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+          
+          <div class="pagination">
+            <ul>
+              <li class="arrow" :class="{ disabled: currentPage === 1 }" @click="changePage(currentPage - 1)">
+                &lt;
+              </li>
+              <li v-for="page in totalPages" :key="page" :class="{ active: currentPage === page }" @click="changePage(page)">
+                {{ page }}
+              </li>
+              <li class="arrow" :class="{ disabled: currentPage === totalPages }" @click="changePage(currentPage + 1)">
+                &gt;
+              </li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { ref } from 'vue';
+import { PlusCircleFilled, CloseOutlined, DownOutlined } from '@ant-design/icons-vue';
+import HrPopup from '../../../component/Popup/HrPopup.vue';
+
+const isOpen = ref(false);
+
+export default {
+  data() {
+    const today = new Date().toISOString().split('T')[0];
+    return {
+      selectedManager: '',
+      showPopup: false,
+      selectedDepartment: null,
+      departmentInfo: {
+        parentDept: '',
+        deptName: '',
+        deptDesc: ''
+      },
+      menus: [
+        {
+          title: '부서1',
+          isOpen: true,
+          submenus: [
+            {
+              label: '직원검색',
+              link: { name: 'hrSearch' },
+            },
+          ],
+        },
+      ],
+      currentPage: 1,
+      totalPages: 3,
+      members: [],
+      h3icon: "/client/resources/img/h3icon.png",
+      require: "/client/resources/img/require.png",
+      menuicon: "/client/resources/img/arrow-rg.png",
+      topmenuicon: "/client/resources/img/topmenu.png",
+      arrow: "/client/resources/img/arrow.png",
+      addtopmenu: "/client/resources/img/addtopmenu.png",
+      addsubmenu: "/client/resources/img/addsubmenu.png",
+    };
+  },
+  
+  components: {
+    PlusCircleFilled, 
+    CloseOutlined, 
+    DownOutlined, 
+    HrPopup
+  },
+  
+  methods: {
+    toggleMenu(index) {
+      this.menus[index].isOpen = !this.menus[index].isOpen;
+    },
+    
+    selectTopDepartment(deptName) {
+      this.selectedDepartment = {
+        parent: null, // 상위부서 없음
+        name: deptName,
+        isTopLevel: true // 최상위 부서임을 표시
+      };
+      
+      this.departmentInfo.parentDept = ''; // 상위부서 비움
+      this.departmentInfo.deptName = deptName;
+    },
+    
+    selectDepartment(parentTitle, deptName) {
+      this.selectedDepartment = {
+        parent: parentTitle,
+        name: deptName,
+        isTopLevel: false
+      };
+      
+      this.departmentInfo.parentDept = parentTitle;
+      this.departmentInfo.deptName = deptName;
+    },
+    
+    addMember(selectedUser) {
+      console.log('addMember 호출됨:', selectedUser); // 디버깅용
+      
+      // 이미 추가된 사용자인지 확인
+      const isAlreadyAdded = this.members.some(member => member.userId === selectedUser.userId);
+
+      if (isAlreadyAdded) {
+        alert('이미 추가된 사용자입니다.');
+        this.showPopup = false;
+        return;
+      }
+
+      this.members.push({
+        userId: selectedUser.userId,
+        position: selectedUser.clsfNm || selectedUser.position, // 직급
+        name: selectedUser.userNm || selectedUser.name, // 이름
+        department: selectedUser.deptNm, // 부서
+        responsibility: selectedUser.rspofcNm, // 직책
+        checked: false
+      });
+      
+      this.showPopup = false;
+      console.log('새로운 멤버 추가됨, 총 멤버 수:', this.members.length); // 디버깅용
+    },
+    
+    removeMember() {
+      this.members = this.members.filter(member => !member.checked);
+    },
+    
+    addTopMenu() {
+      const newIndex = this.menus.length + 1;
+      this.menus.push({
+        title: `부서${newIndex}`,
+        isOpen: false,
+        submenus: [],
+      });
+    },
+    
+    addSubMenu(menuIndex) {
+      this.menus[menuIndex].submenus.push({
+        label: `신규메뉴${this.menus[menuIndex].submenus.length + 1}`,
+        link: { name: 'hrSearch' },
+      });
+    },
+    
+    changePage(page) {
+      if (page >= 1 && page <= this.totalPages) {
+        this.currentPage = page;
+      }
+    },
+    
+    handleSubmit() {
+      console.log('폼 제출:', this.departmentInfo);
+    },
+  },
+  
+  mounted() {
+    // 초기에 첫 번째 하위부서 선택
+    if (this.menus.length > 0 && this.menus[0].submenus.length > 0) {
+      this.selectDepartment(this.menus[0].title, this.menus[0].submenus[0].label);
+    }
+  }
+};
+</script>
+
+<style scoped>
+.card {
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  padding: 24px;
+  margin: 20px;
+}
+
+.card-title {
+  font-size: 24px;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 24px;
+  border-bottom: 2px solid #f0f0f0;
+  padding-bottom: 12px;
+}
+
+.flex {
+  display: flex;
+  gap: 24px;
+}
+
+.align-top {
+  align-items: flex-start;
+}
+
+.sch-form-wrap.search {
+  min-width: 280px;
+  background: #f8f9fa;
+  border-radius: 8px;
+  padding: 16px;
+  border: 1px solid #e9ecef;
+}
+
+.sidemenu {
+  margin-bottom: 12px;
+}
+
+.menu-box {
+  background: white;
+  border-radius: 6px;
+  border: 1px solid #dee2e6;
+  overflow: hidden;
+}
+
+.topmenu {
+  display: flex;
+  align-items: center;
+  padding: 12px 16px;
+  background: #fff;
+  cursor: pointer;
+  transition: background-color 0.2s;
+  border-bottom: 1px solid #e9ecef;
+}
+
+.topmenu:hover {
+  background: #f8f9fa;
+}
+
+.topmenu .arrow {
+  width: 16px;
+  height: 16px;
+  margin-right: 8px;
+  transition: transform 0.2s;
+}
+
+.arrow-open {
+  transform: rotate(90deg);
+}
+
+.topmenu img:not(.arrow) {
+  width: 20px;
+  height: 20px;
+  margin-right: 8px;
+}
+
+.top-dept-name {
+  cursor: pointer;
+  padding: 2px 4px;
+  border-radius: 4px;
+  transition: background-color 0.2s;
+}
+
+.top-dept-name:hover {
+  background: #e9ecef;
+}
+
+.topmenu .top-dept-name.active-top {
+  background: #007bff;
+  color: white;
+}
+
+.topmenu p {
+  flex: 1;
+  margin: 0;
+  font-weight: 500;
+  color: #333;
+}
+
+.btn {
+  padding: 4px 8px;
+  border: none;
+  border-radius: 4px;
+  font-size: 12px;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.btn.secondary {
+  background: #6c757d;
+  color: white;
+}
+
+.btn.secondary:hover {
+  background: #5a6268;
+}
+
+.btn.tertiary {
+  background: #f8f9fa;
+  color: #6c757d;
+  border: 1px solid #dee2e6;
+}
+
+.btn.btn-red {
+  background: #dc3545;
+  color: white;
+}
+
+.btn.btn-red:hover {
+  background: #c82333;
+}
+
+.submenu-list {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  background: #f8f9fa;
+}
+
+.submenu {
+  border-bottom: 1px solid #e9ecef;
+}
+
+.submenu:last-child {
+  border-bottom: none;
+}
+
+.submenu-item {
+  display: flex;
+  align-items: center;
+  padding: 10px 16px;
+  cursor: pointer;
+  transition: background-color 0.2s;
+}
+
+.submenu-item:hover {
+  background: #e9ecef;
+}
+
+.submenu-item.active-link {
+  background: #007bff;
+  color: white;
+}
+
+.submenu-item img {
+  width: 16px;
+  height: 16px;
+  margin-right: 8px;
+}
+
+.submenu-item p {
+  margin: 0;
+  font-size: 14px;
+}
+
+.buttons {
+  margin-top: 16px;
+  text-align: center;
+}
+
+.buttons button {
+  background: none;
+  border: none;
+  cursor: pointer;
+  padding: 8px;
+  border-radius: 4px;
+  transition: background-color 0.2s;
+}
+
+.buttons button:hover {
+  background: #e9ecef;
+}
+
+.title-wrap {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+  padding: 16px;
+  background: #f8f9fa;
+  border-radius: 6px;
+  border: 1px solid #dee2e6;
+}
+
+.title-wrap h3 {
+  display: flex;
+  align-items: center;
+  margin: 0;
+  font-size: 18px;
+  font-weight: 600;
+  color: #333;
+}
+
+.title-wrap h3 img {
+  width: 20px;
+  height: 20px;
+  margin-right: 8px;
+}
+
+.form-label {
+  font-weight: 500;
+  color: #333;
+  margin-bottom: 6px;
+}
+
+.form-label p {
+  margin: 0;
+  display: flex;
+  align-items: center;
+}
+
+.require {
+  margin-left: 4px;
+}
+
+.require img {
+  width: 8px;
+  height: 8px;
+}
+
+.form-control {
+  width: 100%;
+  padding: 8px 12px;
+  border: 1px solid #ced4da;
+  border-radius: 4px;
+  font-size: 14px;
+  transition: border-color 0.2s;
+}
+
+.form-control:focus {
+  outline: none;
+  border-color: #007bff;
+  box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
+}
+
+.form-control[readonly] {
+  background-color: #f8f9fa;
+  color: #6c757d;
+}
+
+.textarea {
+  resize: vertical;
+  min-height: 80px;
+}
+
+.tbl-wrap {
+  margin: 16px 0;
+  border: 1px solid #dee2e6;
+  border-radius: 6px;
+  overflow: hidden;
+}
+
+.tbl {
+  width: 100%;
+  border-collapse: collapse;
+  background: white;
+}
+
+.tbl thead th {
+  background: #f8f9fa;
+  padding: 12px;
+  text-align: center;
+  font-weight: 600;
+  color: #333;
+  border-bottom: 2px solid #dee2e6;
+}
+
+.tbl tbody td {
+  padding: 12px;
+  text-align: center;
+  border-bottom: 1px solid #e9ecef;
+}
+
+.tbl tbody tr:hover {
+  background: #f8f9fa;
+}
+
+.form-check {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.form-check input[type="checkbox"],
+.form-check input[type="radio"] {
+  margin: 0;
+  cursor: pointer;
+}
+
+.pagination {
+  display: flex;
+  justify-content: center;
+  margin-top: 20px;
+}
+
+.pagination ul {
+  display: flex;
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  gap: 4px;
+}
+
+.pagination li {
+  padding: 8px 12px;
+  border: 1px solid #dee2e6;
+  background: white;
+  cursor: pointer;
+  border-radius: 4px;
+  transition: all 0.2s;
+}
+
+.pagination li:hover:not(.disabled) {
+  background: #f8f9fa;
+}
+
+.pagination li.active {
+  background: #007bff;
+  color: white;
+  border-color: #007bff;
+}
+
+.pagination li.disabled {
+  color: #6c757d;
+  cursor: not-allowed;
+  background: #f8f9fa;
+}
+
+.col-12 {
+  margin-bottom: 16px;
+}
+
+.row {
+  display: flex;
+  flex-direction: column;
+}
+
+.pt-3 {
+  padding-top: 16px;
+}
+
+.g-3 {
+  gap: 16px;
+}
+</style>(파일 끝에 줄바꿈 문자 없음)
Add a comment
List