jichoi / calendar star
하관우 하관우 07-30
2025-07-30 하관우 직원관련 추가
@1b9f425c2eeb291b05cedba885eabeacf8d54a0d
client/resources/api/asset.js
--- client/resources/api/asset.js
+++ client/resources/api/asset.js
@@ -1,4 +1,4 @@
-import apiClient from "./index";
+import {apiClient} from "./index";
 
 // 차량 정보 등록
 export const saveAsSetVhcle = insertAsSetVhcleDTO => {
client/resources/api/author.js
--- client/resources/api/author.js
+++ client/resources/api/author.js
@@ -1,4 +1,4 @@
-import apiClient from "./index";
+import {apiClient} from "./index";
 
 // 목록 조회
 export const findAuthorsProc = () => {
client/resources/api/bsrp.js
--- client/resources/api/bsrp.js
+++ client/resources/api/bsrp.js
@@ -1,4 +1,4 @@
-import apiClient from "./index";
+import {apiClient} from "./index";
 
 // 등록
 export const saveBsrpProc = data => {
client/resources/api/code.js
--- client/resources/api/code.js
+++ client/resources/api/code.js
@@ -1,4 +1,4 @@
-import apiClient from "./index";
+import {apiClient} from "./index";
 
 // 등록
 export const saveCodeProc = data => {
client/resources/api/index.js
--- client/resources/api/index.js
+++ client/resources/api/index.js
@@ -1,5 +1,5 @@
 import axios from 'axios';
-import store from "../../views/pages/AppStore";
+import store from "../../views/pages/AppStore"; // Vuex 스토어 임포트
 
 // JWT 토큰 디코더
 function decodeToken(token) {
@@ -15,87 +15,96 @@
         return null;
     }
 }
-
-// 공통 API 클라이언트
-const apiClient = axios.create({
-    baseURL: '/',
-    headers: {
-        'Content-Type': 'application/json; charset=UTF-8',
+// 모든 클라이언트에 적용될 요청 인터셉터
+const requestInterceptor = config => {
+    // skipAuthRefresh는 요청 시점에 config에 직접 추가하여 토큰 재발급 로직을 건너뛰게 할 때 사용
+    if (store.state.authorization) {
+        config.headers.Authorization = store.state.authorization;
     }
-});
+    return config;
+};
 
-// 요청 인터셉터 - 토큰이 있으면 Authorization 헤더 추가
-apiClient.interceptors.request.use(
-    config => {
-        if (store.state.authorization) {
-            config.headers.Authorization = store.state.authorization;
-        }
-        return config;
-    },
-    error => Promise.reject(error)
-);
+// 모든 클라이언트에 적용될 응답 인터셉터
+const responseInterceptor = async error => {
+    const originalReq = error.config;
+    if (!error.response) return Promise.reject(error);
 
-// 응답 인터셉터
-apiClient.interceptors.response.use(
-    response => {
-        return response
-    },
-    async error => {
-        const originalReq = error.config;
-        if (!error.response) return Promise.reject(error);
-
-        // 로그인 요청
-        if (originalReq?.skipAuthRefresh) {
-            return Promise.reject(error);
-        }
-
-        // 권한 없음
-        if (error.response.status === 403 && error.response.data.message === '접근 권한이 없습니다.') {
-            window.history.back();
-            return Promise.reject(error);
-        }
-
-        // 리프레시 토큰 요청
-        if (originalReq.url.includes('/refresh/tknReissue.json')) {
-            return Promise.reject(error);
-        }
-
-        // 토큰 만료 시 한 번만 재시도
-        if (error.response.status === 401 && !originalReq._retry) {
-            originalReq._retry = true;
-            try {
-                const res = await axios.post('/refresh/tknReissue.json',{});
-                const newToken = res.headers.authorization;
-
-                // 토큰 저장
-                store.commit('setAuthorization', newToken);
-                originalReq.headers.Authorization = store.state.authorization;
-
-                // 유저 정보 다시 저장
-                const user = decodeToken(newToken);
-                store.commit("setUserInfo", {
-                    userNm: user.userNm,
-                    loginId: user.loginId,
-                    userId: user.userId,
-                    roles: Array.isArray(user.roles) ? user.roles.map(r => r.authority) : [],
-                });
-
-                // 실패했던 요청 재시도
-                return apiClient(originalReq);
-            } catch (refreshError) {
-                // 리프레시 실패 - 세션 만료 처리
-                sessionStorage.setItem("redirect", window.location.pathname + window.location.search);
-                alert('세션이 종료 되었습니다.\n로그인을 새로 해주세요.');
-                store.commit("setStoreReset");
-                localStorage.clear();
-                sessionStorage.clear();
-                window.location.href = '/login.page';
-                return Promise.reject(refreshError);
-            }
-        }
-
+    // 로그인 요청 (skipAuthRefresh 플래그 확인)
+    if (originalReq?.skipAuthRefresh) {
         return Promise.reject(error);
     }
-);
 
-export default apiClient;
+    // 권한 없음
+    if (error.response.status === 403 && error.response.data.message === '접근 권한이 없습니다.') {
+        alert('접근 권한이 없습니다.'); // 사용자에게 메시지 알림
+        window.history.back(); // 뒤로 가기
+        return Promise.reject(error);
+    }
+
+    // 리프레시 토큰 요청 자체는 재시도하지 않음
+    if (originalReq.url.includes('/refresh/tknReissue.json')) {
+        return Promise.reject(error);
+    }
+
+    // 토큰 만료 시 한 번만 재시도 (401 에러)
+    if (error.response.status === 401 && !originalReq._retry) {
+        originalReq._retry = true; // 재시도 플래그 설정
+        try {
+            // 리프레시 토큰으로 새 토큰 발급 요청
+            const res = await axios.post('/refresh/tknReissue.json', {});
+            const newToken = res.headers.authorization;
+
+            // Vuex 스토어에 새 토큰 저장
+            store.commit('setAuthorization', newToken);
+            originalReq.headers.Authorization = store.state.authorization; // 재시도 요청의 헤더 업데이트
+
+            // 유저 정보 다시 디코딩하여 스토어에 저장 (새 토큰 기준으로)
+            const user = decodeToken(newToken);
+            store.commit("setUserInfo", {
+                userNm: user.userNm,
+                loginId: user.loginId,
+                userId: user.userId,
+                roles: Array.isArray(user.roles) ? user.roles.map(r => r.authority) : [],
+            });
+
+            // 실패했던 원본 요청 재시도
+            return axios(originalReq); // axios 인스턴스에 원래 요청 그대로 전달하여 재시도
+        } catch (refreshError) {
+            // 리프레시 실패 시 (세션 만료 등) 로그인 페이지로 강제 이동
+            sessionStorage.setItem("redirect", window.location.pathname + window.location.search);
+            alert('세션이 종료되었습니다.\n로그인을 새로 해주세요.');
+            store.commit("setStoreReset");
+            localStorage.clear();
+            sessionStorage.clear();
+            window.location.href = '/login.page';
+            return Promise.reject(refreshError);
+        }
+    }
+
+    // 그 외 모든 에러는 그대로 reject
+    return Promise.reject(error);
+};
+
+const createConfiguredClient = (contentType) => {
+    const client = axios.create({
+        baseURL: '/', // 모든 API 요청에 기본으로 붙을 URL
+        headers: {
+            'Content-Type': contentType,
+        },
+    });
+
+    // 요청 및 응답 인터셉터 적용
+    client.interceptors.request.use(requestInterceptor, error => Promise.reject(error));
+    client.interceptors.response.use(response => response, responseInterceptor);
+
+    return client;
+};
+
+// JSON 데이터 요청을 위한 클라이언트
+const apiClient = createConfiguredClient('application/json; charset=UTF-8');
+
+// 멀티파트(파일 업로드) 요청을 위한 클라이언트
+const fileClient = createConfiguredClient('multipart/form-data');
+
+// 모듈 외부로 내보내기
+export { apiClient, fileClient };
(파일 끝에 줄바꿈 문자 없음)
client/resources/api/login.js
--- client/resources/api/login.js
+++ client/resources/api/login.js
@@ -1,4 +1,4 @@
-import apiClient from "./index";
+import {apiClient} from "./index";
 
 export const loginProc = userInfo => {
     return apiClient.post('/user/login.json', userInfo, {skipAuthRefresh: true});
client/resources/api/menu.js
--- client/resources/api/menu.js
+++ client/resources/api/menu.js
@@ -1,4 +1,4 @@
-import apiClient from "./index";
+import {apiClient} from "./index";
 
 // 목록 조회
 export const findMenusProc = () => {
client/resources/api/menuAuthor.js
--- client/resources/api/menuAuthor.js
+++ client/resources/api/menuAuthor.js
@@ -1,4 +1,4 @@
-import apiClient from "./index";
+import {apiClient} from "./index";
 
 // 목록 조회
 export const findMenuAuthorsProc = data => {
client/resources/api/sanctns.js
--- client/resources/api/sanctns.js
+++ client/resources/api/sanctns.js
@@ -1,4 +1,4 @@
-import apiClient from "./index";
+import {apiClient} from "./index";
 
 // 결재 요청 목록 조회
 export const findMyApprovalRequestsProc = data => {
client/resources/api/user.js
--- client/resources/api/user.js
+++ client/resources/api/user.js
@@ -1,6 +1,26 @@
-import apiClient from "./index";
+import { apiClient, fileClient } from "./index";
+import uploadService from "@/resources/js/uploadService";
 
 // 조회 - 목록
 export const findUsersProc = data => {
   return apiClient.get('/user/users.json', { params: data });
-}
(파일 끝에 줄바꿈 문자 없음)
+}
+
+// 스크랩 자료 상세 조회
+export const findUsersDetProc = (userId) => {
+  return apiClient.get(`/user/${userId}/users.json`);
+}
+
+export const joinProc = (formData) => {
+  return fileClient.post(`/user/join.file`, formData);
+}
+
+// 스크랩 자료 수정 (PUT 메서드 사용)
+export const updateUsersProc =  (userId, formData) => {
+  return fileClient.put(`/user/${userId}/users.file`, formData);
+}
+
+// 비밀번호 수정
+export const updatePassword = (userId, updateUserPasswordDTO) => {
+  return apiClient.put(`/user/${userId}/updatePassword.json`, updateUserPasswordDTO);
+}
client/resources/api/vcatn.js
--- client/resources/api/vcatn.js
+++ client/resources/api/vcatn.js
@@ -1,4 +1,4 @@
-import apiClient from "./index";
+import {apiClient} from "./index";
 
 // 등록
 export const saveVcatnProc = data => {
 
client/resources/js/downloadService.js (added)
+++ client/resources/js/downloadService.js
@@ -0,0 +1,257 @@
+// @/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 (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/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/layout/AdminHeader.vue
--- client/views/layout/AdminHeader.vue
+++ client/views/layout/AdminHeader.vue
@@ -18,9 +18,9 @@
         };
     },
     methods: {
-        logout(){
+        logout() {
             localStorage.removeItem('isLoggedIn');
-this.$router.push('/login.page'); 
+            this.$router.push('/login.page');
 
         }
     },
@@ -29,9 +29,9 @@
     components: {
         'Menu': Menu,
     },
-    created() {},
-    mounted() {},
-    beforeUnmount() {},
+    created() { },
+    mounted() { },
+    beforeUnmount() { },
 };
 
 </script>
(파일 끝에 줄바꿈 문자 없음)
client/views/pages/Manager/asset/CardInfoManagement.vue
--- client/views/pages/Manager/asset/CardInfoManagement.vue
+++ client/views/pages/Manager/asset/CardInfoManagement.vue
@@ -167,7 +167,7 @@
         useAll: false,
         listType:"all",
 
-        currentPage: null,
+        currentPage: 1,
         recordSize: 4,
         pageSize: null,
         totalRecordCount: null,
client/views/pages/Manager/hr/hrDetail.vue
--- client/views/pages/Manager/hr/hrDetail.vue
+++ client/views/pages/Manager/hr/hrDetail.vue
@@ -2,108 +2,91 @@
   <div class="card ">
     <div class="card-body ">
       <h2 class="card-title">직원 검색</h2>
-  
-        
-          <div class="name-box flex  simple">
-            <div class="img-area" >
-              <div class="img"><img :src="photoicon" alt="">
-              </div>
-            </div>
-            <form class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }"
-              @submit.prevent="handleRegister" novalidate >
-              <div class="col-12">
-                <label for="yourName" class="form-label">아이디</label>
-                <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly
-                  placeholder="admin">
-              </div>
-              <div class="col-12 ">
-                <div class="col-12 border-x">
-                  <label for="youremail" class="form-label ">이름<p class="require"><img :src="require" alt=""></p></label>
-                  <input v-model="email" type="text" name="username" class="form-control" id="youremail" required
-                    readonly>
-                </div>
-  
-                <div class="col-12 border-x">
-                  <label for="yourPassword" class="form-label">부서</label>
-                  <input v-model="password" type="password" name="password" class="form-control" id="yourPassword"
-                    required readonly placeholder="주식회사 테이큰 소프트">
-                </div>
-              </div>
-              <div class="col-12 border-x">
-                <div class="col-12 border-x">
-                  <label for="youremail" class="form-label">직급</label>
-                  <input v-model="email" type="text" name="username" class="form-control" id="youremail" required readonly
-                    placeholder="과장">
-                </div>
-  
-                <div class="col-12 border-x">
-                  <label for="yourPassword" class="form-label">직책</label>
-                  <input v-model="password" type="password" name="password" class="form-control" id="yourPassword"
-                    required readonly placeholder="팀장">
-                </div>
-              </div>
-  
-  
-            </form>
-  
+      <div class="name-box flex  simple">
+        <div class="img-area">
+          <div class="img"><img :src="selectedUser.thumbnail?.filePath || photoicon" alt="">
           </div>
-          <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }"
-            @submit.prevent="handleRegister" novalidate style="margin-bottom: 20px;">
-            <div class="col-12">
-              <label for="yourName" class="form-label">연락처</label>
-              <input v-model="name" type="text" name="name" class="form-control " id="yourName" required readonly
-                placeholder="admin">
-            </div>
-            <div class="col-12">
-              <label for="yourName" class="form-label">생년월일</label>
-              <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly
-                placeholder="admin">
+        </div>
+        <div class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }"
+          @submit.prevent="handleRegister" novalidate>
+          <div class="col-12">
+            <label for="loginId" class="form-label">아이디</label>
+            <input v-model="selectedUser.loginId" type="text" name="name" class="form-control" id="yourName" required
+              readonly placeholder="admin">
+          </div>
+          <div class="col-12 ">
+            <div class="col-12 border-x">
+              <label for="userNm" class="form-label ">이름<p class="require"><img :src="require" alt=""></p></label>
+              <input v-model="selectedUser.userNm" type="text" name="username" class="form-control" id="youremail"
+                required readonly>
             </div>
             <div class="col-12 border-x">
-              <label for="yourName" class="form-label">입사일</label>
-              <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly
-                placeholder="admin">
+              <label for="deptNm" class="form-label">부서</label>
+              <input v-model="selectedUser.deptNm" type="text" name="password" class="form-control" id="yourPassword"
+                required readonly placeholder="주식회사 테이큰 소프트">
             </div>
-  
-  
-  
-          </form>
-          <form class="row g-3 needs-validation detail salary" :class="{ 'was-validated': formSubmitted }"
-            @submit.prevent="handleRegister" novalidate>
-            <div class=" col-12 border-x"><label>연봉</label>
-              <div class="yearsalary">
-              <div class="col-12">
-                <label for="" class="second-label">2023</label>
-                <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly
-                  placeholder="admin">
-              </div>
-              <div class="col-12">
-                <label for="" class="second-label">2023</label>
-                <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly
-                  placeholder="admin">
-              </div>
-              <div class="col-12 border-x">
-                <label for="" class="second-label">2023</label>
-                <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly
-                  placeholder="admin">
-              </div>
+          </div>
+          <div class="col-12 border-x">
+            <div class="col-12 border-x">
+              <label for="clsfNm" class="form-label">직급</label>
+              <input v-model="selectedUser.clsfNm" type="text" name="username" class="form-control" id="youremail"
+                required readonly>
             </div>
+            <div class="col-12 border-x">
+              <label for="rspofcNm" class="form-label">직책</label>
+              <input v-model="selectedUser.rspofcNm" type="password" name="password" class="form-control"
+                id="yourPassword" required readonly>
             </div>
-          </form>
-          <div class="buttons">
-            <button type="delete" class="btn sm sm btn-red">탈퇴</button>
-        <button type="reset" class="btn sm sm secondary">수정</button>
-        <button type="submit" class="btn sm sm tertiary">목록</button>
+          </div>
+        </div>
+      </div>
+      <div class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }"
+        @submit.prevent="handleRegister" novalidate style="margin-bottom: 20px;">
+        <div class="col-12">
+          <label for="mbtlnum" class="form-label">연락처</label>
+          <input v-model="selectedUser.mbtlnum" type="text" name="name" class="form-control " id="yourName" required
+            readonly placeholder="admin">
+        </div>
+        <div class="col-12">
+          <label for="brthdy" class="form-label">생년월일</label>
+          <input v-model="selectedUser.brthdy" type="text" name="name" class="form-control" id="yourName" required
+            readonly placeholder="admin">
+        </div>
+        <div class="col-12 border-x">
+          <label for="encpn" class="form-label">입사일</label>
+          <input v-model="selectedUser.encpn" type="text" name="name" class="form-control" id="yourName" required
+            readonly placeholder="admin">
+        </div>
+      </div>
+      <div class="row g-3 needs-validation detail salary" :class="{ 'was-validated': formSubmitted }"
+        @submit.prevent="handleRegister" novalidate>
+        <div class=" col-12 border-x"><label>연봉</label>
+          <div class="yearsalary">
+            <div class="col-12" v-for="(salary, index) in selectedUser.salaryList" :key="index">
+              <label for="year" class="second-label">{{ salary.year }}</label>
+              <input v-model="salary.amount" type="text" name="amount" class="form-control" id="amount" required
+                readonly placeholder="admin" />
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="buttons">
+        <button type="delete" class="btn sm sm btn-red" @click="submitDeleteForm"
+          v-if="selectedUser.useAt === 'Y'">탈퇴</button>
+        <button type="delete" class="btn sm sm btn-red" @click="submitRepairForm"
+          v-if="selectedUser.useAt === 'N'">복구</button>
+        <button type="reset" class="btn sm sm secondary" @click="goToDetail"
+          v-if="selectedUser.useAt === 'Y'">수정</button>
+        <button type="submit" class="btn sm sm tertiary" @click="goToPage">목록</button>
       </div>
     </div>
-
   </div>
-
 </template>
 
 <script>
 import GoogleCalendar from "../../../component/GoogleCalendar.vue"
 import { SearchOutlined } from '@ant-design/icons-vue';
+import { findUsersDetProc, updateUsersProc } from "../../../../resources/api/user"
 export default {
   data() {
     return {
@@ -114,61 +97,107 @@
       startbtn: "/client/resources/img/start.png",
       stopbtn: "/client/resources/img/stop.png",
       moreicon: "/client/resources/img/more.png",
-      today: new Date().toLocaleDateString('ko-KR', {
-        year: 'numeric',
-        month: '2-digit',
-        day: '2-digit',
-        weekday: 'short',
-      }),
-      time: this.getCurrentTime(),
-      listData: Array.from({ length: 20 }, (_, i) => ({
-      department: `부서 ${i + 1}`,
-      name: `이름 ${i + 1}`,
-      position: `직급 ${i + 1}`
-    }))
+      selectedUser: {},
     }
   },
   components: {
-    SearchOutlined
+    SearchOutlined, findUsersDetProc
   },
   methods: {
-    formatBudget(amount) {
-      return new Intl.NumberFormat().format(amount) + ' 원';
-    },
-    isPastPeriod(period) {
-      // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출
-      const endDateStr = period.split('~')[1]?.trim();
-      if (!endDateStr) return false;
+    //사용자 정보 상세 조회
+    async searchUser() {
+      try {
+        const response = await findUsersDetProc(this.$route.query.id);
+        if (response.status === 200) {
+          this.selectedUser = response.data.data.user; // API 응답에서 카테고리 목록을 가져옴
 
-      const endDate = new Date(endDateStr);
-      const today = new Date();
+          console.log("유저 아이디", this.selectedUser);
+        }
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
+      }
+    },
+    goToPage() {
+      this.$router.push({ name: 'hrManagement' });
+    },
+    goToDetail() {
+      this.$router.push({
+        name: 'hrInsert',
+        query: { id: this.selectedUser.userId }
+      });
+    },
+    async submitDeleteForm() {
+      const confirmDelete = confirm("정말로 사용자를 탈퇴시키겠습니까?\n이 작업은 되돌릴 수 없습니다.");
 
-      // 현재 날짜보다 과거면 true
-      return endDate < today;
-    },
-    getStatusClass(status) {
-      return status === 'active' ? 'status-active' : 'status-inactive';
-    },
-    getStatusClass(status) {
-      if (status === '미진행') return 'status-pending';
-      if (status === '진행중') return 'status-approved';
+      if (!confirmDelete) { // 사용자가 '아니오'를 눌렀을 때
+        alert("사용자 탈퇴가 취소되었습니다.");
+        return; // 함수 실행 중단
+      }
 
-      // Default empty string
-      return '';
+      // 2. FormData 객체 생성
+      const formData = new FormData();
+
+      // 수정용 DTO 생성 (기존 데이터 + 수정 데이터)
+      const updateDTO = {
+        userNm: this.selectedUser.userNm,
+        useAt: "N",
+        fileId: this.selectedUser.fileId,
+      };
+
+      formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], {
+        type: 'application/json'
+      }));
+
+      // 4. API 통신
+      try {
+        const response = await updateUsersProc(this.$route.query.id, formData);
+
+        if (response.status === 200) {
+          alert('사용자 탈퇴퇴가 성공적으로 저장되었습니다!');
+        }
+      } catch (error) {
+        console.error("사용자 정보 저장 중 오류 발생:", error);
+        alert("사용자 정보 저장 중 오류가 발생했습니다.");
+        if (error.response && error.response.data && error.response.data.message) {
+          alert(`오류: ${error.response.data.message}`);
+        }
+      }
     },
-    getCurrentTime() {
-      const now = new Date();
-      const hours = String(now.getHours()).padStart(2, '0');
-      const minutes = String(now.getMinutes()).padStart(2, '0');
-      const seconds = String(now.getSeconds()).padStart(2, '0');
-      return `${hours}:${minutes}:${seconds}`;
-    },
-    getCategoryClass(category) {
-      switch (category) {
-        case '용역': return 'category-service';
-        case '내부': return 'category-internal';
-        case '국가과제': return 'category-government';
-        default: return '';
+    async submitRepairForm() {
+      const confirmDelete = confirm("정말로 사용자를 복구시키겠습니까?\n이 작업은 되돌릴 수 없습니다.");
+
+      if (!confirmDelete) { // 사용자가 '아니오'를 눌렀을 때
+        alert("사용자 복구구가 취소되었습니다.");
+        return; // 함수 실행 중단
+      }
+
+      // 2. FormData 객체 생성
+      const formData = new FormData();
+
+      // 수정용 DTO 생성 (기존 데이터 + 수정 데이터)
+      const updateDTO = {
+        userNm: this.selectedUser.userNm,
+        useAt: "Y",
+        fileId: this.selectedUser.fileId,
+      };
+
+      formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], {
+        type: 'application/json'
+      }));
+
+      // 4. API 통신
+      try {
+        const response = await updateUsersProc(this.$route.query.id, formData);
+
+        if (response.status === 200) {
+          alert('사용자 탈퇴퇴가 성공적으로 저장되었습니다!');
+        }
+      } catch (error) {
+        console.error("사용자 정보 저장 중 오류 발생:", error);
+        alert("사용자 정보 저장 중 오류가 발생했습니다.");
+        if (error.response && error.response.data && error.response.data.message) {
+          alert(`오류: ${error.response.data.message}`);
+        }
       }
     },
   },
@@ -179,10 +208,7 @@
 
   },
   mounted() {
-    console.log('main mounted');
-    setInterval(() => {
-      this.time = this.getCurrentTime();
-    }, 1000);
+    this.searchUser();
   }
 }
 </script>
client/views/pages/Manager/hr/hrInsert.vue
--- client/views/pages/Manager/hr/hrInsert.vue
+++ client/views/pages/Manager/hr/hrInsert.vue
@@ -2,123 +2,140 @@
   <div class="card ">
     <div class="card-body ">
       <h2 class="card-title">직원 검색</h2>
-  
-        
-          <div class="name-box flex  simple">
-            <div class="img-area column">
+      <div class="name-box flex  simple">
+        <div class="img-area column">
           <div class="img">
-            <img :src="previewImg || placeholder" alt="미리보기 이미지" />
+            <img :src="croppedImg || placeholder" alt="미리보기 이미지" />
             <button class="close-btn" @click="removeImage">&#215;</button>
           </div>
           <div class="info">
             <div class="file">
               <label for="fileUpload" class="file-label">
                 <img :src="file" alt="">
-                <p>업로드</p>
+                <button @click="showCropModal = true">
+                  <p>업로드</p>
+                </button>
               </label>
-              <input id="fileUpload" type="file" @change="handleFileUpload" accept="image/*" />
             </div>
           </div>
         </div>
-            <form class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }"
-              @submit.prevent="handleRegister" novalidate >
-              <div class="col-12 ">
-                <div class="col-12 border-x">
-                  <label for="youremail" class="form-label "><p>아이디<p class="require"><img :src="require" alt=""></p></p></label>
-                  <input v-model="email" type="text" name="username" class="form-control"  required
-                    >
-                </div>
-  
-                <div class="col-12 border-x">
-                  <label for="yourPassword" class="form-label"><p>권한<p class="require"><img :src="require" alt=""></p></p></label>
-                  <select class="form-select" >
-                <option value="선택">선택</option>
-                <option value=""></option>
-                <option value=""></option>
-              </select>
-                </div>
-              </div>
-              <div class="col-12 ">
-                <div class="col-12 border-x">
-                  <label for="youremail" class="form-label "><p>이름<p class="require"><img :src="require" alt=""></p></p></label>
-                  <input v-model="email" type="text" name="username" class="form-control"  required
-                    >
-                </div>
-  
-                <div class="col-12 border-x">
-                  <label for="yourPassword" class="form-label"><p>부서<p class="require"><img :src="require" alt=""></p></p></label>
-                  <input v-model="selectedname" type="password" name="password" class="form-control" 
-                    required  placeholder="주식회사 테이큰 소프트">
-                    <input type="button" class="form-control " value="검색" @click="showPopup = true" />
-                    <BuseoPopup v-if="showPopup" @close="showPopup = false" @select="addApproval"/>
-                </div>
-              </div>
-              <div class="col-12 border-x">
-                <div class="col-12 border-x">
-                  <label for="youremail" class="form-label"><p>직급<p class="require"><img :src="require" alt=""></p></p></label>
-                  <select class="form-select" >
-                <option value="선택">선택</option>
-                <option value=""></option>
-                <option value=""></option>
-              </select>
-                </div>
-  
-                <div class="col-12 border-x">
-                  <label for="yourPassword" class="form-label">직책</label>
-                  <select class="form-select" >
-                <option value="선택">선택</option>
-                <option value=""></option>
-                <option value=""></option>
-              </select>
-                </div>
-              </div>
-  
-  
-            </form>
-  
-          </div>
-          <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }"
-            @submit.prevent="handleRegister" novalidate style="margin-bottom: 20px;">
-            <div class="col-12">
-              <label for="yourName" class="form-label">연락처</label>
-              <input v-model="name" type="text" name="name" class="form-control " id="yourName"  
-                placeholder="admin">
-            </div>
-            <div class="col-12">
-              <label for="yourName" class="form-label">생년월일</label>
-              <input v-model="name" type="date" name="name" class="form-control" id="yourName"  
-               >
+        <ImageCropper :key="cropperKey" field="avatar" v-model="showCropModal" :width="200" :height="200"
+          img-format="png" lang-type="ko" @crop-success="cropSuccess" :no-square="true" :round-cropping="true"
+          :max-size="1024" />
+        <div class="tbl-wrap row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }">
+          <div class="col-12 ">
+            <div class="col-12 border-x">
+              <label for="youremail" class="form-label ">
+                <p>아이디
+                <p class="require"><img :src="require" alt=""></p>
+                </p>
+              </label>
+              <input v-if="isUserIdCheck" v-model="selectedUser.loginId" type="text" name="loginId" class="form-control"
+                required readonly>
+              <input v-else v-model="requestDTO.loginId" type="text" name="loginId" class="form-control" required>
             </div>
             <div class="col-12 border-x">
-              <label for="yourName" class="form-label"><p>입사일<p class="require"><img :src="require" alt=""></p></p></label>
-              <input v-model="name" type="date" name="name" class="form-control" id="yourName" required 
-                >
+              <label for="yourPassword" class="form-label">
+                <p>권한
+                <p class="require"><img :src="require" alt=""></p>
+                </p>
+              </label>
+              <select class="form-select" v-model="selectedAuthor">
+                <option value="">선택</option>
+                <option v-for="author in authorCode" :key="author.authorNm" :value="author.authorCode">
+                  {{ author.authorNm }}
+                </option>
+              </select>
             </div>
-  
-  
-  
-          </form>
-          <form class="row g-3 needs-validation detail salary" :class="{ 'was-validated': formSubmitted }"
-            @submit.prevent="handleRegister" novalidate>
-            <div class=" col-12 border-x"><label>연봉<button type="button" title="추가" @click="addSalary">
+          </div>
+          <div class="col-12 ">
+            <div class="col-12 border-x">
+              <label for="youremail" class="form-label ">
+                <p>이름
+                <p class="require"><img :src="require" alt=""></p>
+                </p>
+              </label>
+              <input v-model="requestDTO.userNm" type="text" name="userNm" class="form-control" required>
+            </div>
+            <div class="col-12 border-x">
+              <label for="yourPassword" class="form-label">
+                <p>부서
+                <p class="require"><img :src="require" alt=""></p>
+                </p>
+              </label>
+              <input v-model="selectedname" type="text" name="text" class="form-control" required>
+              <input type="button" class="form-control " value="검색" @click="showPopup = true" />
+              <BuseoPopup v-if="showPopup" @close="showPopup = false" @select="addApproval" />
+            </div>
+          </div>
+          <div class="col-12 border-x">
+            <div class="col-12 border-x">
+              <label for="youremail" class="form-label">
+                <p>직급
+                <p class="require"><img :src="require" alt=""></p>
+                </p>
+              </label>
+              <select class="form-select" v-model="selectedClsf">
+                <option value="">선택</option>
+                <option v-for="jobRank in clsfCode" :key="jobRank.codeNm" :value="jobRank.code">
+                  {{ jobRank.codeNm }}
+                </option>
+              </select>
+            </div>
+            <div class="col-12 border-x">
+              <label for="yourPassword" class="form-label">직책</label>
+              <select class="form-select" v-model="selectedRepofc">
+                <option value="">선택</option>
+                <option v-for="position in rspofcCode" :key="position.codeNm" :value="position.code">
+                  {{ position.codeNm }}
+                </option>
+              </select>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="tbl-wrap row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }"
+        style="margin-bottom: 20px;">
+        <div class="col-12">
+          <label for="phone" class="form-label">연락처</label>
+          <input v-model="requestDTO.mbtlnum" type="text" name="mbtlnum" class="form-control" id="phone"
+            placeholder="010-1234-5678" @input="filterPhoneNumber">
+        </div>
+        <div class="col-12">
+          <label for="birth" class="form-label">생년월일</label>
+          <input v-model="requestDTO.brthdy" type="date" name="brthdy" class="form-control" id="yourName">
+        </div>
+        <div class="col-12 border-x">
+          <label for="yourName" class="form-label">
+            <p>입사일
+            <p class="require"><img :src="require" alt=""></p>
+            </p>
+          </label>
+          <input v-model="requestDTO.encpn" type="date" name="encpn" class="form-control" id="yourName" required>
+        </div>
+      </div>
+      <div class="tbl-wrap row g-3 needs-validation detail salary" :class="{ 'was-validated': formSubmitted }">
+        <div class=" col-12 border-x"><label>연봉<button type="button" title="추가" @click="addSalary">
               <PlusCircleFilled />
             </button></label>
-              <div class="yearsalary approval-container">
-              <div class="col-12 border-x addapproval" v-for="(salary, index) in salarys" :key="index">
-                <input type="text" name="name" class="form-control" v-model="salary.salary" style="width: 200px;"  
-                  placeholder="년도">
-                <div>
-                  <input type="text" name="name" class="form-control" v-model="salary.total"   
-                    placeholder="금액">
-                </div>
+          <div class="yearsalary approval-container">
+            <div class="col-12 border-x addapproval" v-for="(salary, index) in salarys" :key="index">
+              <input type="text" name="name" class="form-control" v-model="salary.year" style="width: 200px;"
+                placeholder="년도" maxlength="4" @input="validateYearInput(salary)">
+              <div>
+                <input type="text" name="name" class="form-control" v-model="salary.amount" placeholder="금액">
               </div>
-              
+              <button type="button" @click="removeSalary(index)" class="delete-button">
+                <CloseOutlined />
+              </button>
             </div>
-            </div>
-          </form>
-          <div class="buttons">
-        <button type="reset" class="btn sm sm primary">등록</button>
-        <button type="submit" class="btn sm sm tertiary">취소</button>
+          </div>
+        </div>
+      </div>
+      <div class="buttons">
+        <button type="button" class="btn sm primary" @click="submitUpdateForm" v-if="isUserIdCheck">수정</button>
+        <button type="button" class="btn sm primary" @click="submitForm" v-else>등록</button>
+        <button type="button" class="btn sm tertiary" @click="goToManagementList">취소</button>
       </div>
     </div>
 
@@ -128,18 +145,24 @@
 
 <script>
 import GoogleCalendar from "../../../component/GoogleCalendar.vue"
-import { SearchOutlined, PlusCircleFilled } from '@ant-design/icons-vue';
+import { SearchOutlined, PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue';
 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";
+
 export default {
   data() {
     return {
       showPopup: false,
       selectedname: '',
       approvals: [],
-      salarys: [ {
-          salary: '',
-          total: '',
-        },] ,
+      salarys: [{
+        year: '',
+        amount: '',
+      },],
       previewImg: null,
       placeholder: "/client/resources/img/img1.png",
       require: "/client/resources/img/require.png",
@@ -151,112 +174,418 @@
       startbtn: "/client/resources/img/start.png",
       stopbtn: "/client/resources/img/stop.png",
       moreicon: "/client/resources/img/more.png",
-      today: new Date().toLocaleDateString('ko-KR', {
-        year: 'numeric',
-        month: '2-digit',
-        day: '2-digit',
-        weekday: 'short',
-      }),
-      time: this.getCurrentTime(),
       listData: Array.from({ length: 20 }, (_, i) => ({
-      department: `부서 ${i + 1}`,
-      name: `이름 ${i + 1}`,
-      position: `직급 ${i + 1}`
-    }))
+        department: `부서 ${i + 1}`,
+        name: `이름 ${i + 1}`,
+        position: `직급 ${i + 1}`
+      })),
+
+      // 코드 조회
+      serachRequest: {
+        searchType: "upperCd",
+        searchText: null,
+      },
+      // 직급코드 
+      clsfCode: [],
+      // 직책코드
+      rspofcCode: [],
+      // 권한 코드
+      authorCode: [],
+
+      showCropModal: false, // ImageCropper 모달을 보여줄지 말지 결정하는 변수
+      croppedImg: null,     // 최종 크롭되어 표시될 이미지의 Data URL 또는 서버 이미지 URL
+
+      cropperKey: 0,
+      requestDTO: {
+        loginId: null,      // @NotBlank, @Size, @Pattern
+        clsf: null,         // 선택된 직급의 코드 (예: "RESP_JRM")
+        rspofc: null,       // 선택된 직책의 코드 (예: "RSP_DR")
+        userNm: null,       // @NotBlank, @Size
+        password: null,     // @NotBlank, @Size, @Pattern (이 화면에서 password 받는다면)
+        mbtlnum: null,      // 휴대폰
+        telno: null,        // 전화번호 (현재 화면에 없으면 null 유지)
+        email: null,        // 이메일 (현재 화면에 없으면 null 유지)
+        zip: null,          // 우편번호 (현재 화면에 없으면 null 유지)
+        adres: null,        // 주소 (현재 화면에 없으면 null 유지)
+        detailAdres: null,  // 상세주소 (현재 화면에 없으면 null 유지)
+        fileId: null,       // 파일 아이디 (파일 저장 후 백엔드에서 반환될 값, 직접 입력X)
+        brthdy: null,       // 생년월일 (YYYY-MM-DD 문자열)
+        encpn: null,        // 입사일 (YYYY-MM-DD 문자열)
+        authorList: [],     // 선택된 권한의 ID들을 담을 배열 (UserAuthorVO 리스트 형태로 변환하여 전송)
+      },
+      selectedAuthor: '',
+      selectedClsf: '',
+      selectedRepofc: '',
+
+
+
+      copyAuthor: null,
+      selectedUser: {},
+      isUserIdCheck: false,
     }
   },
   components: {
-    SearchOutlined, PlusCircleFilled, BuseoPopup
+    SearchOutlined, PlusCircleFilled, BuseoPopup, CloseOutlined, findAuthorsProc, ImageCropper, FileUploadProgress, joinProc, findCodesProc
+  },
+  created() {
+    if (this.$route.query.id != null) {
+      this.searchUser();
+    };
+    this.clsfTypeCodes(); // 직급 종류 조회
+    this.rspofcTypeCodes(); // 직책 종류 조회
+    this.authorTypeCodes(); // 권한 종류 조회
   },
   methods: {
-    addApproval(selectedUser) {
+    //연도 4자리 숫자만 기록록
+    validateYearInput(salaryItem) {
+      // 1. 숫자 이외의 문자 제거 (정규식 사용)
+      salaryItem.year = salaryItem.year.replace(/[^0-9]/g, '');
+
+      // 2. 길이가 4자를 초과하면 잘라내기
+      if (salaryItem.year.length > 4) {
+        salaryItem.year = salaryItem.year.slice(0, 4);
+      }
+      // console.log("Current year value:", salaryItem.year); // 디버깅용
+    },
+    dataURLtoFile(dataurl, filename) {
+      var arr = dataurl.split(','),
+        mime = arr[0].match(/:(.*?);/)[1],
+        bstr = atob(arr[1]),
+        n = bstr.length,
+        u8arr = new Uint8Array(n);
+      while (n--) {
+        u8arr[n] = bstr.charCodeAt(n);
+      }
+      return new File([u8arr], filename, { type: mime });
+    },
+
+    // 연락처 숫자만 기록
+    filterPhoneNumber() {
+      if (this.requestDTO.mbtlnum) {
+        this.requestDTO.mbtlnum = this.requestDTO.mbtlnum.replace(/\D/g, '');
+      }
+    },
+    // 취소 누를 시 직원관리 페이지로 이동
+    goToManagementList() {
+      this.$router.push('/hr-management/hrManagement.page');
+    },
+    // 공통코드 직급
+    async clsfTypeCodes() {
+      this.clsfCode = []; // 초기화
+      this.serachRequest.searchType = "UPPER_CODE"
+      this.serachRequest.searchText = "clsf_code"; // 공통코드 분류코드 (직급)
+
+      try {
+        const response = await findCodesProc(this.serachRequest);
+        if (response.status === 200) {
+          this.clsfCode = response.data.data.codes; // API 응답에서 카테고리 목록을 가져옴
+        }
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
+      }
+    },
+    // 공통코드 직책
+    async rspofcTypeCodes() {
+      this.clsfCode = []; // 초기화
+      this.serachRequest.searchType = "UPPER_CODE"
+      this.serachRequest.searchText = "rspofc_code"; // 공통코드 분류코드 (직급)
+
+      try {
+        const response = await findCodesProc(this.serachRequest);
+        if (response.status === 200) {
+          this.rspofcCode = response.data.data.codes; // API 응답에서 카테고리 목록을 가져옴
+        }
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
+      }
+    },
+    // 공통코드 권한
+    async authorTypeCodes() {
+      this.authorCode = []; // 초기화
+      try {
+        const response = await findAuthorsProc();
+        if (response.status === 200) {
+          this.authorCode = response.data.data.authors; // API 응답에서 카테고리 목록을 가져옴
+        }
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
+      }
+    },
+    addApproval(user) {
       this.approvals.push({
-        name: selectedUser.name
+        name: user.name
       });
 
-      this.selectedname = selectedUser.name; // 입력창에 표시
+      this.selectedname = user.department; // 입력창에 표시
       this.showPopup = false;
     },
     addSalary() {
       this.salarys.push({
-        salary: '',
-        total: '',
+        year: '',
+        amount: '',
       });
     },
-    handleFileUpload(event) {
-      const file = event.target.files[0];
-      if (file && file.type.startsWith('image/')) {
-        const reader = new FileReader();
-        reader.onload = (e) => {
-          this.previewImg = e.target.result; // 파일 읽기 완료 후 미리보기 이미지 설정
-        };
-        reader.readAsDataURL(file); // 파일을 데이터 URL로 읽기
-      } else {
-        alert('이미지 파일만 선택할 수 있습니다.');
+    removeSalary(index) {
+      this.salarys.splice(index, 1);
+    },
+    removeImage() {
+      this.croppedImg = null;
+      this.cropperKey++;
+    },
+    cropSuccess(imgDataUrl) {
+      this.croppedImg = imgDataUrl;
+      this.showCropModal = false;
+    },
+    //등록용
+    async submitForm() {
+      // 1. 프론트엔드 유효성 검사 (네 style인 alert + return 방식)
+      if (this.$isEmpty(this.requestDTO.loginId)) { alert("아이디를 입력해 주세요."); return; }
+      if (this.$isEmpty(this.requestDTO.userNm)) { alert("이름을 입력해 주세요."); return; }
+      if (this.$isEmpty(this.requestDTO.clsf)) { alert("직급을 선택해 주세요."); return; }
+      if (this.$isEmpty(this.requestDTO.encpn)) { alert("입사일을 선택해 주세요."); return; }
+      if (this.$isEmpty(this.requestDTO.authorList) || this.requestDTO.authorList.length === 0) {
+        alert("권한을 선택해 주세요."); return;
+      }
+
+      // 2. FormData 객체 생성 및 데이터 추가
+      const formData = new FormData();
+
+      formData.append('joinDTO', new Blob([JSON.stringify(this.requestDTO)], {
+        type: 'application/json'
+      }));
+
+      formData.append('salarys', new Blob([JSON.stringify(this.salarys)], {
+        type: 'application/json'
+      }));
+
+      // 파일이 있다면 File 객체로 변환하여 'multipartFiles' 필드명으로 추가
+      if (this.croppedImg) {
+        const profileImageFile = this.dataURLtoFile(this.croppedImg, 'profile_image.png');
+        formData.append("multipartFiles", profileImageFile);
+      }
+
+      // 3. API 통신 (fileClient 사용)
+      try {
+        const response = await joinProc(formData);
+
+        if (response.status === 200) {
+          alert('사용자 정보가 성공적으로 저장되었습니다!');
+          // 성공 시 페이지 이동 (예: this.$router.push('/hr-management/hrManagement.page'))
+        }
+      } catch (error) {
+        console.error("사용자 정보 저장 중 오류 발생:", error);
+        alert("사용자 정보 저장 중 오류가 발생했습니다.");
+        if (error.response && error.response.data && error.response.data.message) {
+          alert(`오류: ${error.response.data.message}`);
+        }
       }
     },
+    //수정용
+    async submitUpdateForm() {
+      // 1. 프론트엔드 유효성 검사
+      if (this.$isEmpty(this.requestDTO.userNm)) { alert("이름을 입력해 주세요."); return; }
+      if (this.$isEmpty(this.requestDTO.clsf)) { alert("직급을 선택해 주세요."); return; }
+      if (this.$isEmpty(this.requestDTO.encpn)) { alert("입사일을 선택해 주세요."); return; }
+      if (this.$isEmpty(this.requestDTO.authorList) || this.requestDTO.authorList.length === 0) {
+        alert("권한을 선택해 주세요."); return;
+      }
 
-    // 이미지 삭제 함수
-    removeImage() {
-      this.previewImg = null; // 미리보기 이미지 삭제
-      this.$refs.fileUpload.value = null; // 파일 input 초기화
-    },
-    formatBudget(amount) {
-      return new Intl.NumberFormat().format(amount) + ' 원';
-    },
-    isPastPeriod(period) {
-      // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출
-      const endDateStr = period.split('~')[1]?.trim();
-      if (!endDateStr) return false;
+      // 2. FormData 객체 생성
+      const formData = new FormData();
+      let updateDTO = {};
 
-      const endDate = new Date(endDateStr);
-      const today = new Date();
+      if (this.selectedAuthor == this.copyAuthor) {
+        // 수정용 DTO 생성 (기존 데이터 + 수정 데이터)
+        updateDTO = {
+          ...this.requestDTO,
+          salaryList: this.salarys,
+          authorUpdateCheck: false,
+        };
+      } else {
+        updateDTO = {
+          ...this.requestDTO,
+          salaryList: this.salarys,
+          authorUpdateCheck: true,
+        };
+      }
 
-      // 현재 날짜보다 과거면 true
-      return endDate < today;
-    },
-    getStatusClass(status) {
-      return status === 'active' ? 'status-active' : 'status-inactive';
-    },
-    getStatusClass(status) {
-      if (status === '미진행') return 'status-pending';
-      if (status === '진행중') return 'status-approved';
+      formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], {
+        type: 'application/json'
+      }));
 
-      // Default empty string
-      return '';
+      // 3. 파일 처리
+      // 기존 파일이 그대로 유지되는 경우 (fileId만 전송)
+      if (this.selectedUser.fileId && !this.croppedImg.startsWith('data:')) {
+        // 기존 파일 ID 유지
+        updateDTO.fileId = this.selectedUser.fileId;
+      }
+      // 새로운 파일이 업로드된 경우
+      else if (this.croppedImg && this.croppedImg.startsWith('data:')) {
+        const profileImageFile = this.dataURLtoFile(this.croppedImg, 'profile_image.png');
+        formData.append("multipartFiles", profileImageFile);
+        // 기존 파일 ID 제거 (새 파일로 교체)
+        updateDTO.fileId = null;
+      }
+
+      // 4. API 통신
+      try {
+        const response = await updateUsersProc(this.$route.query.id, formData);
+
+        if (response.status === 200) {
+          alert('사용자 정보가 성공적으로 저장되었습니다!');
+          // 성공 시 페이지 이동 (예: this.$router.push('/hr-management/hrManagement.page'))
+        }
+      } catch (error) {
+        console.error("사용자 정보 저장 중 오류 발생:", error);
+        alert("사용자 정보 저장 중 오류가 발생했습니다.");
+        if (error.response && error.response.data && error.response.data.message) {
+          alert(`오류: ${error.response.data.message}`);
+        }
+      }
     },
-    getCurrentTime() {
-      const now = new Date();
-      const hours = String(now.getHours()).padStart(2, '0');
-      const minutes = String(now.getMinutes()).padStart(2, '0');
-      const seconds = String(now.getSeconds()).padStart(2, '0');
-      return `${hours}:${minutes}:${seconds}`;
-    },
-    getCategoryClass(category) {
-      switch (category) {
-        case '용역': return 'category-service';
-        case '내부': return 'category-internal';
-        case '국가과제': return 'category-government';
-        default: return '';
+    //사용자 정보 상세 조회
+    async searchUser() {
+      try {
+        const response = await findUsersDetProc(this.$route.query.id);
+        if (response.status === 200) {
+          this.selectedUser = response.data.data.user; // API 응답에서 카테고리 목록을 가져옴
+
+          this.requestDTO = {
+            userNm: this.selectedUser.userNm,
+            clsf: this.selectedUser.clsf,
+            rspofc: this.selectedUser.rspofc || '',
+            mbtlnum: this.selectedUser.mbtlnum,
+            telno: this.selectedUser.telno,
+            email: this.selectedUser.email,
+            zip: this.selectedUser.zip,
+            adres: this.selectedUser.adres,
+            detailAdres: this.selectedUser.detailAdres,
+            brthdy: this.selectedUser.brthdy,
+            encpn: this.selectedUser.encpn,
+            userSttus: this.selectedUser.userSttus,
+            useAt: this.selectedUser.useAt,
+            fileId: this.selectedUser.fileId, // 기존 파일 ID 보존
+            authorList: this.selectedUser.authorList
+          }
+          this.selectedAuthor = this.selectedUser.authorList[0]?.authorCode;
+          this.selectedClsf = this.selectedUser.clsf;
+          this.selectedRepofc = this.selectedUser.rspofc || '';
+
+          this.croppedImg = this.selectedUser.thumbnail?.filePath || null;
+
+          // 연봉 정보 설정
+          this.salarys = this.selectedUser.salaryList || [];
+
+          this.copyAuthor = this.selectedUser.authorList[0]?.authorCode;
+          this.isUserIdCheck = true;
+          console.log("유저 정보 로드 완료", this.selectedUser);
+        }
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
       }
     },
   },
   watch: {
-
+    // 직급 선택 감지 -> requestDTO.clsf 업데이트
+    selectedClsf(newVal) {
+      this.requestDTO.clsf = newVal ? newVal : null; // 선택된 직급 객체의 'code'를 requestDTO.clsf에 할당
+    },
+    // 직책 선택 감지 -> requestDTO.rspofc 업데이트
+    selectedRepofc(newVal) {
+      this.requestDTO.rspofc = newVal ? newVal : null; // 선택된 직책 객체의 'code'를 requestDTO.rspofc에 할당
+    },
+    // 권한 선택 감지 -> requestDTO.authorList 업데이트
+    selectedAuthor(newVal) {
+      if (newVal) {
+        // UserAuthorVO 형태에 맞춰 authorId와 authorNm을 포함하는 객체를 생성하여 배열에 담음
+        // 백엔드 UserAuthorVO에 정확히 authorId, authorNm 필드가 있는지 확인 필요
+        this.requestDTO.authorList = [{
+          authorCode: newVal,
+        }];
+      } else {
+        this.requestDTO.authorList = [];
+      }
+    },
   },
   computed: {
-
   },
   mounted() {
-    console.log('main mounted');
-    setInterval(() => {
-      this.time = this.getCurrentTime();
-    }, 1000);
-  }
+  },
 }
 </script>
 <style scoped>
 tr {
   cursor: pointer;
 }
+
+.profile-container {
+  padding: 20px;
+  text-align: center;
+}
+
+.info {
+  margin-bottom: 20px;
+}
+
+.profile-image-area {
+  width: 150px;
+  height: 150px;
+  border-radius: 50%;
+  /* 원형으로 만들게 */
+  overflow: hidden;
+  /* 영역 밖으로 넘치는 이미지 자르기 */
+  margin: 0 auto 20px;
+  /* 가운데 정렬 및 아래 여백 */
+  border: 2px solid #eee;
+  /* 테두리 */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #f9f9f9;
+  /* 기본 배경색 */
+}
+
+.cropped-profile-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  /* 이미지를 영역에 꽉 채우되 비율 유지 */
+}
+
+.default-profile-icon {
+  font-size: 60px;
+  color: #ccc;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+}
+
+.remove-btn {
+  display: inline-block;
+  /* 버튼 형태로 보이기 위해 */
+  padding: 10px 20px;
+  margin: 5px;
+  background-color: #007bff;
+  color: white;
+  border: none;
+  border-radius: 5px;
+  cursor: pointer;
+  font-size: 16px;
+  transition: background-color 0.3s ease;
+}
+
+.file-label.upload-btn:hover,
+.remove-btn:hover {
+  background-color: #0056b3;
+}
+
+.remove-btn {
+  background-color: #dc3545;
+}
+
+.remove-btn:hover {
+  background-color: #c82333;
+}
 </style>
(파일 끝에 줄바꿈 문자 없음)
client/views/pages/Manager/hr/hrManagement.vue
--- client/views/pages/Manager/hr/hrManagement.vue
+++ client/views/pages/Manager/hr/hrManagement.vue
@@ -8,15 +8,24 @@
             <select name="" id="" class="form-select">
               <option value="">부서</option>
             </select>
-            <select name="" id="" class="form-select">
+            <select class="form-select" v-model="selectedClsf">
               <option value="">직급</option>
+              <option v-for="jobRank in clsfCode" :key="jobRank.codeNm" :value="jobRank">
+                {{ jobRank.codeNm }}
+              </option>
             </select>
-            <select name="" id="" class="form-select">
+            <select class="form-select" v-model="selectedRepofc">
               <option value="">직책</option>
+              <option v-for="position in rspofcCode" :key="position.codeNm" :value="position">
+                {{ position.codeNm }}
+              </option>
             </select>
             <div class="sch-input">
-              <input type="text" class="form-control" placeholder="직원명">
-              <button class="ico-sch"><SearchOutlined /></button>
+              <input type="text" class="form-control" v-model="userSearchReqDTO.searchText" @keyup.enter="searchUsers"
+                placeholder="직원명">
+              <button class="ico-sch" @click="searchUsers">
+                <SearchOutlined />
+              </button>
             </div>
           </div>
         </div>
@@ -38,43 +47,25 @@
             </thead>
             <!-- 동적으로 <td> 생성 -->
             <tbody>
-              <tr v-for="(item, index) in listData" :key="index"  @click="goToDetailPage(item)"> 
-                <td>{{ item.department }}</td>
-      <td>{{ item.position }}</td>
-      <td>{{ item.role }}</td>
-      <td>{{ item.userId }}</td>
-      <td>{{ item.name }}</td>
-      <td>{{ item.joinDate }}</td>
-      <td>
-        <button class="btn sm tertiary xsm" @click.stop="resetPassword(item)">초기화</button>
-      </td>
+              <tr v-for="(item, index) in selectedUsers" :key="index" @click="goToDetailPage(item)" :class="{ 'inactive-user': item.useAt === 'N' }">
+                <td>{{ item.deptNm }}</td>
+                <td>{{ item.clsfNm }}</td>
+                <td>{{ item.rspofcNm }}</td>
+                <td>{{ item.loginId }}</td>
+                <td>{{ item.userNm }}</td>
+                <td>{{ item.encpn }}</td>
+                <td>
+                  <button class="btn tertiary xsm" @click.stop="resetPassword(item)">초기화</button>
+                </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>
+        <Pagination :search="userSearchReqDTO" @onChange="fnChangeCurrentPage" />
         <div class="buttons">
-                <button class="btn sm primary"  type="submit"  @click="goToPage('등록')">등록</button>
-              </div>
+          <button class="btn primary" type="submit" @click="goToPage('등록')">등록</button>
+        </div>
       </div>
     </div>
   </div>
@@ -83,6 +74,9 @@
 <script>
 import { ref } from 'vue';
 import { SearchOutlined } from '@ant-design/icons-vue';
+import { findUsersProc, updatePassword } from "../../../../resources/api/user"
+import Pagination from '../../../component/Pagination.vue';
+import { findCodesProc } from '../../../../resources/api/code';
 export default {
   data() {
     return {
@@ -90,86 +84,143 @@
       currentPage: 1,
       totalPages: 3,
       photoicon: "/client/resources/img/photo_icon.png",
-      // 데이터 초기화
-      years: [2023, 2024, 2025], // 연도 목록
-      months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록
-      selectedYear: '',
-      selectedMonth: '',
-      listData: [
-  {
-    department: '인사팀',
-    position: '대리',
-    role: '인사담당',
-    userId: 'honggildong',
-    name: '홍길동',
-    joinDate: '2022-01-15'
-  },
-],
-      filteredData: [],
+      selectedUsers: [],
+      // 코드 조회
+      serachRequest: {
+        searchType: "upperCd",
+        searchText: null,
+      },
+      // 직급코드 
+      clsfCode: [],
+      // 직책코드
+      rspofcCode: [],
+      selectedClsf: '',
+      selectedRepofc: '',
+      userSearchReqDTO: {
+        searchType: "all",    // 'id' 또는 'nm' 등 선택
+        searchText: null,    // 검색어 입력
+        userSttus: null,     // 회원상태 선택 (0,1,2,3)
+        useAt: null,         // 사용여부 선택 (Y, N)
+        dept: null,          // 부서 선택
+        clsf: null,          // 직급 선택
+        rspofc: null,         // 직책 선택
+
+        currentPage: 1,
+      },
+      updatePasswordDTO: {
+        oldPassword:"Ts0511@@1",
+        newPassword:"Ts0511@@1",
+        resetAt: true,
+      },
     };
   },
-  components:{
-    SearchOutlined
+  components: {
+    SearchOutlined, Pagination, findCodesProc, updatePassword
   },
   computed: {
   },
+  created() {
+    this.clsfTypeCodes(); // 직급 종류 조회
+    this.rspofcTypeCodes(); // 직책 종류 조회
+  },
   methods: {
-    resetPassword(user) {
-    // 예: 비밀번호 초기화 로직
-    console.log(`${user.name} (${user.userId}) 비밀번호 초기화`);
-    // 실제 초기화 API 호출 또는 처리 추가
-  },
-  goToDetailPage(item) {
-    // item.id 또는 다른 식별자를 사용하여 URL을 구성할 수 있습니다.
-    this.$router.push({ name: 'hrDetail', query: { id: item.id } });
-  },
-    changePage(page) {
-      if (page < 1 || page > this.totalPages) return;
-      this.currentPage = page;
-      this.$emit('page-changed', page); // 필요 시 부모에 알림
+    // 페이지 이동
+    fnChangeCurrentPage(currentPage) {
+      this.userSearchReqDTO.currentPage = Number(currentPage);
+      console.log(this.userSearchReqDTO);
+      this.$nextTick(() => {
+        this.searchUsers();
+      });
     },
-    async onClickSubmit() {
-      // `useMutation` 훅을 사용하여 mutation 함수 가져오기
-      const { mutate, onDone, onError } = useMutation(mygql);
+    //사용자 정보 전체 조회
+    async searchUsers() {
+      try {
+        const response = await findUsersProc(this.userSearchReqDTO);
+        if (response.status === 200) {
+          this.selectedUsers = response.data.data.users; // API 응답에서 카테고리 목록을 가져옴
+          console.log("user 정보 ",this.selectedUsers);
+        }
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
+      }
+    },
+    // 공통코드 직급
+    async clsfTypeCodes() {
+      this.clsfCode = []; // 초기화
+      this.serachRequest.searchType = "UPPER_CODE"
+      this.serachRequest.searchText = "clsf_code"; // 공통코드 분류코드 (직급)
 
       try {
-        const result = await mutate();
-        console.log(result);
+        const response = await findCodesProc(this.serachRequest);
+        if (response.status === 200) {
+          this.clsfCode = response.data.data.codes; // API 응답에서 카테고리 목록을 가져옴
+        }
       } catch (error) {
-        console.error('Mutation error:', error);
+        console.error("검색 중 오류 발생:", error);
       }
+    },
+    // 공통코드 직책
+    async rspofcTypeCodes() {
+      this.clsfCode = []; // 초기화
+      this.serachRequest.searchType = "UPPER_CODE"
+      this.serachRequest.searchText = "rspofc_code"; // 공통코드 분류코드 (직급)
+
+      try {
+        const response = await findCodesProc(this.serachRequest);
+        if (response.status === 200) {
+          this.rspofcCode = response.data.data.codes; // API 응답에서 카테고리 목록을 가져옴
+        }
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
+      }
+    },
+    async resetPassword(user) {
+      try {
+        const response = await updatePassword(user.userId, this.updatePasswordDTO);
+        if (response.status === 200) {
+          console.log("비밀번호 초기화");
+        }
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
+      }
+    },
+    goToDetailPage(item) {
+      // item.id 또는 다른 식별자를 사용하여 URL을 구성할 수 있습니다.
+      this.$router.push({ name: 'hrDetail', query: { id: item.userId } });
     },
     goToPage(type) {
       if (type === '등록') {
         this.$router.push({ name: 'hrInsert' });
       }
     },
-    getStatusClass(status) {
-      if (status === '승인') return 'status-approved';
-      if (status === '대기') return 'status-pending';
-      return '';
-    },
-    isPastPeriod(period) {
-      // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출
-      const endDateStr = period.split('~')[1]?.trim();
-      if (!endDateStr) return false;
-
-      const endDate = new Date(endDateStr);
-      const today = new Date();
-
-      // 현재 날짜보다 과거면 true
-      return endDate < today;
-    }
   },
-  created() {
+  watch: {
+    // 직급 선택 감지 -> requestDTO.clsf 업데이트
+    selectedClsf(newVal) {
+      this.userSearchReqDTO.clsf = newVal ? newVal.code : null; // 선택된 직급 객체의 'code'를 requestDTO.clsf에 할당
+    },
+    // 직책 선택 감지 -> requestDTO.rspofc 업데이트
+    selectedRepofc(newVal) {
+      this.userSearchReqDTO.rspofc = newVal ? newVal.code : null; // 선택된 직책 객체의 'code'를 requestDTO.rspofc에 할당
+    },
   },
   mounted() {
-
-
+    this.searchUsers();
+    this.clsfTypeCodes();
+    this.rspofcTypeCodes();
   },
 };
 </script>
 
 <style scoped>
-tr{cursor: pointer;}
+tr {
+  cursor: pointer;
+}
+.inactive-user {
+  color: #999999 !important; /* 회색 텍스트 */
+}
+
+.inactive-user td {
+  color: #999999 !important; /* 모든 td 요소를 회색으로 */
+}
 </style>
client/views/pages/Manager/hr/hrSearch.vue
--- client/views/pages/Manager/hr/hrSearch.vue
+++ client/views/pages/Manager/hr/hrSearch.vue
@@ -6,8 +6,8 @@
         <div class="sch-form-wrap search">
           <div class="input-group">
             <div class="sch-input">
-              <input type="text" class="form-control" placeholder="직원명">
-              <button class="ico-sch">
+              <input type="text" class="form-control" placeholder="직원명" v-model="searchReqDTO.searchText" @keyup.enter="searchUsers">
+              <button class="ico-sch" @click="searchUsers">
                 <SearchOutlined />
               </button>
             </div>
@@ -24,93 +24,90 @@
               </thead>
               <!-- 동적으로 <td> 생성 -->
               <tbody>
-                <tr v-for="(item, index) in listData" :key="index">
-                  <td></td>
-                  <td></td>
-                  <td></td>
+                <tr v-for="(item, index) in selectedUsers" :key="index" @click="selectUser(item)" :class="{ 'inactive-user': item.useAt === 'N' }">
+                  <td>{{ item.deptNm }}</td>
+                  <td>{{ item.userNm }}</td>
+                  <td>
+                    <template v-if="item.rspofcNm">
+                      {{ item.clsfNm }}({{ item.rspofcNm }})
+                    </template>
+                    <template v-else>
+                      {{ item.clsfNm }}
+                    </template>
+                  </td>
                 </tr>
               </tbody>
             </table>
-  
           </div>
         </div>
-  
         <div>
           <div class="name-box flex sb simple">
             <div class="img-area" style="width: 170px;">
-              <div class="img"><img :src="photoicon" alt="">
+              <div class="img"><img :src="selectedUser.file.filePath" :alt="selectedUser.file.fileNm">
               </div>
             </div>
-            <form class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }"
+            <div class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }"
               @submit.prevent="handleRegister" novalidate>
               <div class="col-12">
                 <label for="yourName" class="form-label">아이디</label>
-                <input v-model="name" type="text" name="name" class="form-control"  required readonly
+                <input v-model="name" type="text" name="name" class="form-control" required readonly
                   placeholder="admin">
               </div>
               <div class="col-12 ">
                 <div class="col-12 border-x">
-                  <label for="youremail" class="form-label ">이름<p class="require"><img :src="require" alt=""></p></label>
-                  <input v-model="email" type="text" name="username" class="form-control"  required
+                  <label for="userNm" class="form-label ">이름<p class="require"><img :src="require" alt=""></p>
+                  </label>
+                  <input v-model="selectedUser.userNm" type="text" name="username" class="form-control" required
                     readonly>
                 </div>
-  
                 <div class="col-12 border-x">
-                  <label for="yourPassword" class="form-label">부서</label>
-                  <input v-model="password" type="password" name="password" class="form-control" 
-                    required readonly placeholder="주식회사 테이큰 소프트">
+                  <label for="deptNm" class="form-label">부서</label>
+                  <input v-model="selectedUser.deptNm" type="text" name="password" class="form-control" required
+                    readonly placeholder="주식회사 테이큰 소프트">
                 </div>
               </div>
               <div class="col-12 border-x">
                 <div class="col-12 border-x">
-                  <label for="youremail" class="form-label">직급</label>
-                  <input v-model="email" type="text" name="username" class="form-control"  required readonly
-                    placeholder="과장">
+                  <label for="clsfNm" class="form-label">직급</label>
+                  <input v-model="selectedUser.clsfNm" type="text" name="username" class="form-control" required
+                    readonly>
                 </div>
-  
                 <div class="col-12 border-x">
-                  <label for="yourPassword" class="form-label">직책</label>
-                  <input v-model="password" type="password" name="password" class="form-control" 
-                    required readonly placeholder="팀장">
+                  <label for="rspofcNm" class="form-label">직책</label>
+                  <input v-model="selectedUser.rspofcNm" type="password" name="password" class="form-control" required
+                    readonly>
                 </div>
               </div>
-  
-  
-            </form>
-  
+            </div>
           </div>
-          <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }"
+          <div class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }"
             @submit.prevent="handleRegister" novalidate>
             <div class="col-12">
-              <label for="yourName" class="form-label">연락처</label>
-              <input v-model="name" type="text" name="name" class="form-control"  required readonly
+              <label for="mbtlnum" class="form-label">연락처</label>
+              <input v-model="selectedUser.mbtlnum" type="text" name="name" class="form-control" required readonly
                 placeholder="admin">
             </div>
             <div class="col-12">
-              <label for="yourName" class="form-label">생년월일</label>
-              <input v-model="name" type="text" name="name" class="form-control" required readonly
+              <label for="brthdy" class="form-label">생년월일</label>
+              <input v-model="selectedUser.brthdy" type="text" name="name" class="form-control" required readonly
                 placeholder="admin">
             </div>
             <div class="col-12 border-x">
-              <label for="yourName" class="form-label">입사일</label>
-              <input v-model="name" type="text" name="name" class="form-control"  required readonly
+              <label for="encpn" class="form-label">입사일</label>
+              <input v-model="selectedUser.encpn" type="text" name="name" class="form-control" required readonly
                 placeholder="admin">
             </div>
-  
-  
-  
-          </form>
+          </div>
         </div>
       </div>
     </div>
-
   </div>
-
 </template>
 
 <script>
 import GoogleCalendar from "../../../component/GoogleCalendar.vue"
 import { SearchOutlined } from '@ant-design/icons-vue';
+import { findUsersProc } from "../../../../resources/api/user"
 export default {
   data() {
     return {
@@ -121,63 +118,64 @@
       startbtn: "/client/resources/img/start.png",
       stopbtn: "/client/resources/img/stop.png",
       moreicon: "/client/resources/img/more.png",
-      today: new Date().toLocaleDateString('ko-KR', {
-        year: 'numeric',
-        month: '2-digit',
-        day: '2-digit',
-        weekday: 'short',
-      }),
-      time: this.getCurrentTime(),
-      listData: Array.from({ length: 20 }, (_, i) => ({
-      department: `부서 ${i + 1}`,
-      name: `이름 ${i + 1}`,
-      position: `직급 ${i + 1}`
-    }))
+      selectedUsers: [],
+      selectedUser: {
+        file: {
+          fileNm: "기본이미지",
+          filePath: "/client/resources/img/photo_icon.png",
+        },
+        loginId: null,
+        userNm: null,
+        deptNm: null,
+        clsfNm: null,
+        rspofcNm: null,
+        mbtlnum: null,
+        brthdy: null,
+        encpn: null,
+      },
+      searchReqDTO: {
+        searchType: "nm",
+        searchText: null,
+      },
     }
   },
   components: {
-    SearchOutlined
+    SearchOutlined, findUsersProc
   },
   methods: {
-    formatBudget(amount) {
-      return new Intl.NumberFormat().format(amount) + ' 원';
-    },
-    isPastPeriod(period) {
-      // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출
-      const endDateStr = period.split('~')[1]?.trim();
-      if (!endDateStr) return false;
-
-      const endDate = new Date(endDateStr);
-      const today = new Date();
-
-      // 현재 날짜보다 과거면 true
-      return endDate < today;
-    },
-    getStatusClass(status) {
-      return status === 'active' ? 'status-active' : 'status-inactive';
-    },
-    getStatusClass(status) {
-      if (status === '미진행') return 'status-pending';
-      if (status === '진행중') return 'status-approved';
-
-      // Default empty string
-      return '';
-    },
-    getCurrentTime() {
-      const now = new Date();
-      const hours = String(now.getHours()).padStart(2, '0');
-      const minutes = String(now.getMinutes()).padStart(2, '0');
-      const seconds = String(now.getSeconds()).padStart(2, '0');
-      return `${hours}:${minutes}:${seconds}`;
-    },
-    getCategoryClass(category) {
-      switch (category) {
-        case '용역': return 'category-service';
-        case '내부': return 'category-internal';
-        case '국가과제': return 'category-government';
-        default: return '';
+    //사용자 정보 전체 조회
+    async searchUsers() {
+      try {
+        const response = await findUsersProc(this.searchReqDTO);
+        if (response.status === 200) {
+          this.selectedUsers = response.data.data.users; // API 응답에서 카테고리 목록을 가져옴
+          console.log("user~~~~~!", this.selectedUsers);
+        }
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
       }
     },
+    selectUser(item) {
+      this.selectedUser = {
+        file: {  // file 객체도 함께 초기화
+          fileNm: "기본이미지",
+          filePath: this.photoicon,
+        },
+        loginId: item.loginId,
+        userNm: item.userNm,
+        deptNm: item.deptNm,
+        clsfNm: item.clsfNm,
+        rspofcNm: item.rspofcNm,
+        mbtlnum: item.mbtlnum,
+        brthdy: item.brthdy,
+        encpn: item.encpn,
+      };
+      // 썸네일 정보가 있으면 업데이트
+      if (item.thumbnail && Object.keys(item.thumbnail).length > 0) {
+        this.selectedUser.file.fileNm = item.thumbnail.fileNm; // thumbnail -> item.thumbnail
+        this.selectedUser.file.filePath = item.thumbnail.filePath; // thumbnail -> item.thumbnail
+      }
+    }
   },
   watch: {
 
@@ -186,10 +184,7 @@
 
   },
   mounted() {
-    console.log('main mounted');
-    setInterval(() => {
-      this.time = this.getCurrentTime();
-    }, 1000);
+    this.searchUsers();
   }
 }
 </script>
@@ -197,4 +192,11 @@
 tr {
   cursor: pointer;
 }
+.inactive-user {
+  color: #999999 !important; /* 회색 텍스트 */
+}
+
+.inactive-user td {
+  color: #999999 !important; /* 모든 td 요소를 회색으로 */
+}
 </style>
(파일 끝에 줄바꿈 문자 없음)
client/views/pages/User/MyPage.vue
--- client/views/pages/User/MyPage.vue
+++ client/views/pages/User/MyPage.vue
@@ -4,19 +4,23 @@
       <div class="name-box">
         <div class="img-area">
           <div class="img">
-            <img :src="previewImg || placeholder" alt="미리보기 이미지" />
+            <img :src="croppedImg || placeholder" alt="미리보기 이미지" />
             <button class="close-btn" @click="removeImage">&#215;</button>
           </div>
           <div class="info">
             <div class="file">
               <label for="fileUpload" class="file-label">
                 <img :src="file" alt="">
-                <p>업로드</p>
+                <button @click="showCropModal = true">
+                  <p>업로드</p>
+                </button>
               </label>
-              <input id="fileUpload" type="file" @change="handleFileUpload" accept="image/*" />
             </div>
           </div>
         </div>
+        <ImageCropper :key="cropperKey" field="avatar" v-model="showCropModal" :width="200" :height="200"
+          img-format="png" lang-type="ko" @crop-success="cropSuccess" :no-square="true" :round-cropping="true"
+          :max-size="1024" />
       </div>
     </div>
   </div>
@@ -24,7 +28,6 @@
 
     <div class="d-flex justify-content-center py-4">
       <a href="index.html" class="logo d-flex align-items-center w-auto">
-        <!-- <span class="d-none d-lg-block"> <img :src="logo" alt=""></span> -->
       </a>
     </div><!-- End Logo -->
 
@@ -35,72 +38,70 @@
         <form class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }"
           @submit.prevent="handleRegister" novalidate>
           <div class="col-12">
-            <label for="yourName" class="form-label">아이디</label>
-            <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly
-              placeholder="admin">
+            <label for="loginId" class="form-label">아이디</label>
+            <input v-model="selectedUser.loginId" type="text" name="name" class="form-control" id="yourName" required
+              readonly placeholder="admin">
           </div>
           <div class="col-12 ">
             <div class="col-12 border-x">
-              <label for="youremail" class="form-label ">
+              <label for="userNm" class="form-label ">
                 <p>이름
                 <p class="require"><img :src="require" alt=""></p>
                 </p>
               </label>
-              <input v-model="email" type="text" name="username" class="form-control" id="youremail" required>
+              <input v-model="requestDTO.userNm" type="text" name="username" class="form-control" id="youremail"
+                required>
             </div>
 
             <div class="col-12 border-x">
-              <label for="yourPassword" class="form-label">부서</label>
-              <input v-model="password" type="password" name="password" class="form-control" id="yourPassword" required
-                readonly placeholder="주식회사 테이큰 소프트">
+              <label for="deptNm" class="form-label">부서</label>
+              <input v-model="selectedUser.deptNm" type="password" name="password" class="form-control"
+                id="yourPassword" required readonly placeholder="부서">
             </div>
           </div>
           <div class="col-12">
             <div class="col-12 border-x">
-              <label for="youremail" class="form-label">직급</label>
-              <input v-model="email" type="text" name="username" class="form-control" id="youremail" required readonly
-                placeholder="과장">
+              <label for="clsfNm" class="form-label">직급</label>
+              <input v-model="selectedUser.clsfNm" type="text" name="username" class="form-control" id="youremail"
+                required readonly placeholder="직급">
             </div>
 
             <div class="col-12 border-x">
-              <label for="yourPassword" class="form-label">직책</label>
-              <input v-model="password" type="password" name="password" class="form-control" id="yourPassword" required
-                readonly placeholder="팀장">
+              <label for="rspofcNm" class="form-label">직책</label>
+              <input v-model="selectedUser.rspofcNm" type="password" name="password" class="form-control"
+                id="yourPassword" required readonly placeholder="직책">
             </div>
           </div>
           <div class="col-12">
-            <label for="yourName" class="form-label">연락처</label>
-            <input v-model="name" type="text" name="name" class="form-control" id="yourName" required>
+            <label for="mbtlnum" class="form-label">연락처</label>
+            <input v-model="requestDTO.mbtlnum" type="text" name="name" class="form-control" id="yourName" required>
           </div>
           <div class="col-12">
-            <label for="yourName" class="form-label">생년월일</label>
-            <input v-model="name" type="text" name="name" class="form-control" id="yourName" required>
+            <label for="brthdy" class="form-label">생년월일</label>
+            <input v-model="requestDTO.brthdy" type="text" name="name" class="form-control" id="yourName" required>
           </div>
           <div class="col-12">
             <label for="yourName" class="form-label">입사일</label>
-            <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly
-              placeholder="2025-01-01">
+            <input v-model="requestDTO.encpn" type="text" name="name" class="form-control" id="yourName" required
+              readonly placeholder="2025-01-01">
           </div>
           <div class="col-12">
-            <label for="yourName" class="form-label">현재 비밀번호</label>
-            <input v-model="name" type="text" name="name" class="form-control" id="yourName" required>
+            <label for="oldPassword" class="form-label">현재 비밀번호</label>
+            <input v-model="updatePasswordDTO.oldPassword" type="password" name="name" class="form-control"
+              id="yourName" required>
           </div>
           <div class="col-12">
-            <label for="yourName" class="form-label">새 비밀번호</label>
+            <label for="newPassword" class="form-label">새 비밀번호</label>
             <div class="box">
-              <input v-model="name" type="text" name="name" class="form-control" id="yourName" required>
+              <input v-model="updatePasswordDTO.newPassword" type="password" name="name" class="form-control"
+                id="yourName" required>
               <div class="invalid-feedback">※ 비밀번호는 6~12자의 영문자와 숫자, 특수기호조합으로 작성해주세요.</div>
             </div>
           </div>
-          <div class="col-12 border-x">
-            <label for="yourName" class="form-label">연락처</label>
-            <input v-model="name" type="text" name="name" class="form-control" id="yourName" required>
-          </div>
-
         </form>
         <div class="buttons">
-          <button class="btn sm btn-red " type="submit">회원탈퇴</button>
-          <button class="btn sm secondary " type="submit">수정</button>
+          <button class="btn sm btn-red " type="submit" @clcick="submitDeleteForm">회원탈퇴</button>
+          <button class="btn sm secondary " type="submit" @click="submitUpdateForm">수정</button>
         </div>
       </div>
     </div>
@@ -109,6 +110,9 @@
 </template>
 
 <script>
+import ImageCropper from 'vue-image-crop-upload'; //이미지 자르기기
+import { findUsersDetProc, updateUsersProc, updatePassword } from "../../../resources/api/user";
+
 export default {
   data() {
     return {
@@ -116,80 +120,308 @@
       placeholder: "/client/resources/img/img1.png",
       require: "/client/resources/img/require.png",
       file: "/client/resources/img/file.png",
-      name: '',
-      email: '',
-      password: '',
-      dept: '',
-      level: '',
       acceptTerms: false,
       formSubmitted: false,
+      userData: null,
+      croppedImg: null,     // 최종 크롭되어 표시될 이미지의 Data URL 또는 서버 이미지 URL
+      cropperKey: 0,
+      showCropModal: false, // ImageCropper 모달을 보여줄지 말지 결정하는 변수
+      selectedUser: {}, //내정보 
+      requestDTO: {
+        loginId: null,      // @NotBlank, @Size, @Pattern
+        clsf: null,         // 선택된 직급의 코드 (예: "RESP_JRM")
+        rspofc: null,       // 선택된 직책의 코드 (예: "RSP_DR")
+        userNm: null,       // @NotBlank, @Size
+        password: null,     // @NotBlank, @Size, @Pattern (이 화면에서 password 받는다면)
+        mbtlnum: null,      // 휴대폰
+        telno: null,        // 전화번호 (현재 화면에 없으면 null 유지)
+        email: null,        // 이메일 (현재 화면에 없으면 null 유지)
+        zip: null,          // 우편번호 (현재 화면에 없으면 null 유지)
+        adres: null,        // 주소 (현재 화면에 없으면 null 유지)
+        detailAdres: null,  // 상세주소 (현재 화면에 없으면 null 유지)
+        fileId: null,       // 파일 아이디 (파일 저장 후 백엔드에서 반환될 값, 직접 입력X)
+        brthdy: null,       // 생년월일 (YYYY-MM-DD 문자열)
+        encpn: null,        // 입사일 (YYYY-MM-DD 문자열)
+        authorList: [],     // 선택된 권한의 ID들을 담을 배열 (UserAuthorVO 리스트 형태로 변환하여 전송)
+      },
+      updatePasswordDTO: {
+        oldPassword: "",
+        newPassword: "",
+        resetAt: false,
+      },
     };
   },
+  components: {
+    ImageCropper, findUsersDetProc, updateUsersProc, updatePassword
+  },
   methods: {
-    handleFileUpload(event) {
-      const file = event.target.files[0];
-      if (file && file.type.startsWith('image/')) {
-        const reader = new FileReader();
-        reader.onload = (e) => {
-          this.previewImg = e.target.result; // 파일 읽기 완료 후 미리보기 이미지 설정
-        };
-        reader.readAsDataURL(file); // 파일을 데이터 URL로 읽기
-      } else {
-        alert('이미지 파일만 선택할 수 있습니다.');
+    dataURLtoFile(dataurl, filename) {
+      var arr = dataurl.split(','),
+        mime = arr[0].match(/:(.*?);/)[1],
+        bstr = atob(arr[1]),
+        n = bstr.length,
+        u8arr = new Uint8Array(n);
+      while (n--) {
+        u8arr[n] = bstr.charCodeAt(n);
       }
+      return new File([u8arr], filename, { type: mime });
     },
+    //사용자 정보 상세 조회
+    async searchUser() {
+      try {
+        const response = await findUsersDetProc(this.userData.userId);
+        if (response.status === 200) {
+          this.selectedUser = response.data.data.user; // API 응답에서 카테고리 목록을 가져옴
 
-    // 이미지 삭제 함수
-    removeImage() {
-      this.previewImg = null; // 미리보기 이미지 삭제
-      this.$refs.fileUpload.value = null; // 파일 input 초기화
-    },
-    handleRegister() {
-      this.formSubmitted = true;
-      // 이메일과 비밀번호가 빈 값이 아니어야 한다
-      if (!this.email.includes('@')) {
-        alert('이메일은 @를 포함해야 합니다.');
-        return; // Stop further processing if email is invalid
-      }
+          this.requestDTO = {
+            userNm: this.selectedUser.userNm,
+            clsf: this.selectedUser.clsf,
+            rspofc: this.selectedUser.rspofc || '',
+            mbtlnum: this.selectedUser.mbtlnum,
+            telno: this.selectedUser.telno,
+            email: this.selectedUser.email,
+            zip: this.selectedUser.zip,
+            adres: this.selectedUser.adres,
+            detailAdres: this.selectedUser.detailAdres,
+            brthdy: this.selectedUser.brthdy,
+            encpn: this.selectedUser.encpn,
+            userSttus: this.selectedUser.userSttus,
+            useAt: this.selectedUser.useAt,
+            fileId: this.selectedUser.fileId, // 기존 파일 ID 보존
+            authorList: this.selectedUser.authorList
+          }
+          this.croppedImg = this.selectedUser.thumbnail?.filePath || null;
 
-      console.log('Email:', this.email);
-      console.log('Password:', this.password);
-      console.log('Name:', this.name);
-      console.log('Accept Terms:', this.acceptTerms);
-      if (this.email && this.password && this.name && this.acceptTerms && this.dept && this.level) {
-        // 로컬 스토리지에 회원가입 정보 저장
-        const userData = {
-          name: this.name,
-          email: this.email,
-          password: this.password,
-          dept: this.dept,
-          level: this.level,
-        };
-
-        console.log('User Data to be saved:', userData);
-        try {
-          localStorage.setItem("UserInfo", JSON.stringify(userData));
-          alert('회원가입이 완료되었습니다!');
-
-          // Redirect to login page
-          this.$router.push("/login");
-        } catch (error) {
-          console.error("Error saving to localStorage:", error);
-          alert("회원가입 중 오류가 발생했습니다.");
+          console.log("유저 아이디", this.selectedUser);
         }
-      } else {
-        alert("모든 필드를 입력해주세요.");
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
+      }
+    },
+    //이미지 지우기
+    removeImage() {
+      this.croppedImg = null;
+      this.cropperKey++;
+    },
+    //이미지 저장
+    cropSuccess(imgDataUrl) {
+      this.croppedImg = imgDataUrl;
+      this.showCropModal = false;
+    },
+    //회원탈퇴
+    async submitDeleteForm() {
+      const confirmDelete = confirm("정말로 사용자를 탈퇴시키겠습니까?\n이 작업은 되돌릴 수 없습니다.");
+
+      if (!confirmDelete) { // 사용자가 '아니오'를 눌렀을 때
+        alert("사용자 탈퇴가 취소되었습니다.");
+        return; // 함수 실행 중단
+      }
+
+      // 2. FormData 객체 생성
+      const formData = new FormData();
+
+      // 수정용 DTO 생성 (기존 데이터 + 수정 데이터)
+      const updateDTO = {
+        userNm: this.selectedUser.userNm,
+        useAt: "N",
+        fileId: this.selectedUser.fileId,
+      };
+
+      formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], {
+        type: 'application/json'
+      }));
+
+      // 4. API 통신
+      try {
+        const response = await updateUsersProc(this.$route.query.id, formData);
+
+        if (response.status === 200) {
+          alert('사용자 탈퇴퇴가 성공적으로 저장되었습니다!');
+        }
+      } catch (error) {
+        console.error("사용자 정보 저장 중 오류 발생:", error);
+        alert("사용자 정보 저장 중 오류가 발생했습니다.");
+        if (error.response && error.response.data && error.response.data.message) {
+          alert(`오류: ${error.response.data.message}`);
+        }
+      }
+    },
+    //수정용
+    async submitUpdateForm() {
+      // 1. 기본 유효성 검사
+      if (this.$isEmpty(this.requestDTO.userNm)) {
+        alert("이름을 입력해 주세요.");
+        return;
+      }
+
+      const oldPwEmpty = this.$isEmpty(this.updatePasswordDTO.oldPassword);
+      const newPwEmpty = this.$isEmpty(this.updatePasswordDTO.newPassword);
+
+      // 2. 비밀번호 입력 상황별 분기 처리
+      if (!oldPwEmpty && newPwEmpty) {
+        alert("새 비밀번호를 입력해주세요.");
+        return;
+      }
+
+      if (oldPwEmpty && !newPwEmpty) {
+        alert("기존 비밀번호를 입력해주세요.");
+        return;
+      }
+
+      if (oldPwEmpty && newPwEmpty) {
+        const confirmUpdate = confirm("비밀번호 변경 없이 내 정보를 수정하시겠습니까?");
+        if (!confirmUpdate) {
+          return; // 취소 시 함수 종료
+        }
+      }
+      console.log("비밀번호 변경 여부:", !oldPwEmpty && !newPwEmpty);
+
+      // 2. FormData 객체 생성
+      const formData = new FormData();
+
+      const updateDTO = {
+        ...this.requestDTO,
+      };
+
+      formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], {
+        type: 'application/json'
+      }));
+
+      // 3. 파일 처리
+      // 기존 파일이 그대로 유지되는 경우 (fileId만 전송)
+      if (this.selectedUser.fileId && !this.croppedImg.startsWith('data:')) {
+        // 기존 파일 ID 유지
+        updateDTO.fileId = this.selectedUser.fileId;
+      }
+      // 새로운 파일이 업로드된 경우
+      else if (this.croppedImg && this.croppedImg.startsWith('data:')) {
+        const profileImageFile = this.dataURLtoFile(this.croppedImg, 'profile_image.png');
+        formData.append("multipartFiles", profileImageFile);
+        // 기존 파일 ID 제거 (새 파일로 교체)
+        updateDTO.fileId = null;
+      }
+
+      // 4. API 통신
+      try {
+        const response = await updateUsersProc(this.userData.userId, formData);
+
+        if (response.status === 200) {
+          if (!oldPwEmpty && !newPwEmpty) {
+            await this.resetPassword();
+          } else {
+            alert('사용자 정보가 성공적으로 저장되었습니다!');
+          }
+        }
+      } catch (error) {
+        console.error("사용자 정보 저장 중 오류 발생:", error);
+        alert("사용자 정보 저장 중 오류가 발생했습니다.");
+        if (error.response && error.response.data && error.response.data.message) {
+          alert(`오류: ${error.response.data.message}`);
+        }
+      }
+    },
+
+    async resetPassword() {
+      try {
+        const response = await updatePassword(this.userData.userId, this.updatePasswordDTO);
+        if (response.status === 200) {
+          alert("내 정보가 수정되었습니다.");
+        }
+      } catch (error) {
+        console.error("검색 중 오류 발생:", error);
       }
     },
   },
   created() {
-    // 로컬스토리지에서 데이터 불러오기
-    const storedData = localStorage.getItem('DeptData');
+    const storedData = localStorage.getItem('vuex'); // 'vuex' 키로 바꿔야 함
     if (storedData) {
-      this.DeptData = JSON.parse(storedData);
+      const parsedData = JSON.parse(storedData);
+      // vuex 스토어 구조에 따라 userInfo 경로 찾아야 함
+      if (parsedData && parsedData.userInfo) {
+        this.userData = parsedData.userInfo;
+        console.log("userData 확인:", this.userData);
+      } else {
+        console.log("vuex 스토어에 userInfo가 없습니다.");
+      }
+    } else {
+      console.log("로컬스토리지에 vuex 데이터가 없습니다.");
     }
+  },
+  mounted() {
+    this.searchUser();
   },
 };
 </script>
 
-<style scoped></style>
(파일 끝에 줄바꿈 문자 없음)
+<style scoped>
+.profile-container {
+  padding: 20px;
+  text-align: center;
+}
+
+.info {
+  margin-bottom: 20px;
+}
+
+.profile-image-area {
+  width: 150px;
+  height: 150px;
+  border-radius: 50%;
+  /* 원형으로 만들게 */
+  overflow: hidden;
+  /* 영역 밖으로 넘치는 이미지 자르기 */
+  margin: 0 auto 20px;
+  /* 가운데 정렬 및 아래 여백 */
+  border: 2px solid #eee;
+  /* 테두리 */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #f9f9f9;
+  /* 기본 배경색 */
+}
+
+.cropped-profile-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  /* 이미지를 영역에 꽉 채우되 비율 유지 */
+}
+
+.default-profile-icon {
+  font-size: 60px;
+  color: #ccc;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 100%;
+  height: 100%;
+}
+
+.remove-btn {
+  display: inline-block;
+  /* 버튼 형태로 보이기 위해 */
+  padding: 10px 20px;
+  margin: 5px;
+  background-color: #007bff;
+  color: white;
+  border: none;
+  border-radius: 5px;
+  cursor: pointer;
+  font-size: 16px;
+  transition: background-color 0.3s ease;
+}
+
+.file-label.upload-btn:hover,
+.remove-btn:hover {
+  background-color: #0056b3;
+}
+
+.remove-btn {
+  background-color: #dc3545;
+}
+
+.remove-btn:hover {
+  background-color: #c82333;
+}
+</style>
(파일 끝에 줄바꿈 문자 없음)
package-lock.json
--- package-lock.json
+++ package-lock.json
@@ -27,6 +27,7 @@
         "simple-datatables": "^9.2.1",
         "url-loader": "4.1.1",
         "vue": "3.5.13",
+        "vue-image-crop-upload": "^3.0.3",
         "vue-loader": "^17.0.0",
         "vue-router": "4.1.5",
         "vue-style-loader": "4.1.3",
@@ -2460,6 +2461,22 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
+      "license": "MIT",
+      "dependencies": {
+        "core-js": "^2.4.0",
+        "regenerator-runtime": "^0.11.0"
+      }
+    },
+    "node_modules/babel-runtime/node_modules/regenerator-runtime": {
+      "version": "0.11.1",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+      "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+      "license": "MIT"
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -3090,6 +3107,14 @@
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
       "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
+      "license": "MIT"
+    },
+    "node_modules/core-js": {
+      "version": "2.6.12",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
+      "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
+      "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
+      "hasInstallScript": true,
       "license": "MIT"
     },
     "node_modules/core-util-is": {
@@ -7787,6 +7812,15 @@
         }
       }
     },
+    "node_modules/vue-image-crop-upload": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/vue-image-crop-upload/-/vue-image-crop-upload-3.0.3.tgz",
+      "integrity": "sha512-VeBsU0oI1hXeCvdpnu19DM/r3KTlI8SUXTxsHsU4MhDXR0ahRziiL9tf4FbILGx+gRVNZhGbl32yuM6TiaGNhA==",
+      "license": "ISC",
+      "dependencies": {
+        "babel-runtime": "^6.11.6"
+      }
+    },
     "node_modules/vue-loader": {
       "version": "17.4.2",
       "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.4.2.tgz",
package.json
--- package.json
+++ package.json
@@ -22,6 +22,7 @@
     "simple-datatables": "^9.2.1",
     "url-loader": "4.1.1",
     "vue": "3.5.13",
+    "vue-image-crop-upload": "^3.0.3",
     "vue-loader": "^17.0.0",
     "vue-router": "4.1.5",
     "vue-style-loader": "4.1.3",
webpack.config.js
--- webpack.config.js
+++ webpack.config.js
@@ -13,6 +13,13 @@
     app: `${BASE_DIR}/client/views/index.js`,
   },
 
+  resolve: {
+    alias: {
+      '@': path.resolve(__dirname, 'client/'),
+    },
+    extensions: ['.js', '.vue', '.json'],
+  },
+
   module: {
     rules: [
       {
Add a comment
List