
Merge branch 'master' of http://210.180.118.83/jichoi/calendar
@ffbf5d9cd6d04dcb81f9ed088db257eee1751f82
--- client/resources/api/asset.js
+++ client/resources/api/asset.js
... | ... | @@ -1,4 +1,4 @@ |
1 |
-import apiClient from "./index"; |
|
1 |
+import {apiClient} from "./index"; |
|
2 | 2 |
|
3 | 3 |
// 차량 정보 등록 |
4 | 4 |
export const saveAsSetVhcle = insertAsSetVhcleDTO => { |
--- client/resources/api/bsrp.js
+++ client/resources/api/bsrp.js
... | ... | @@ -1,4 +1,4 @@ |
1 |
-import apiClient from "./index"; |
|
1 |
+import {apiClient} from "./index"; |
|
2 | 2 |
|
3 | 3 |
// 등록 |
4 | 4 |
export const saveBsrpProc = data => { |
--- client/resources/api/code.js
+++ client/resources/api/code.js
... | ... | @@ -1,4 +1,4 @@ |
1 |
-import apiClient from "./index"; |
|
1 |
+import {apiClient} from "./index"; |
|
2 | 2 |
|
3 | 3 |
// 등록 |
4 | 4 |
export const saveCodeProc = data => { |
--- client/resources/api/index.js
+++ client/resources/api/index.js
... | ... | @@ -1,5 +1,5 @@ |
1 | 1 |
import axios from 'axios'; |
2 |
-import store from "../../views/pages/AppStore"; |
|
2 |
+import store from "../../views/pages/AppStore"; // Vuex 스토어 임포트 |
|
3 | 3 |
|
4 | 4 |
// JWT 토큰 디코더 |
5 | 5 |
function decodeToken(token) { |
... | ... | @@ -15,87 +15,96 @@ |
15 | 15 |
return null; |
16 | 16 |
} |
17 | 17 |
} |
18 |
- |
|
19 |
-// 공통 API 클라이언트 |
|
20 |
-const apiClient = axios.create({ |
|
21 |
- baseURL: '/', |
|
22 |
- headers: { |
|
23 |
- 'Content-Type': 'application/json; charset=UTF-8', |
|
18 |
+// 모든 클라이언트에 적용될 요청 인터셉터 |
|
19 |
+const requestInterceptor = config => { |
|
20 |
+ // skipAuthRefresh는 요청 시점에 config에 직접 추가하여 토큰 재발급 로직을 건너뛰게 할 때 사용 |
|
21 |
+ if (store.state.authorization) { |
|
22 |
+ config.headers.Authorization = store.state.authorization; |
|
24 | 23 |
} |
25 |
-}); |
|
24 |
+ return config; |
|
25 |
+}; |
|
26 | 26 |
|
27 |
-// 요청 인터셉터 - 토큰이 있으면 Authorization 헤더 추가 |
|
28 |
-apiClient.interceptors.request.use( |
|
29 |
- config => { |
|
30 |
- if (store.state.authorization) { |
|
31 |
- config.headers.Authorization = store.state.authorization; |
|
32 |
- } |
|
33 |
- return config; |
|
34 |
- }, |
|
35 |
- error => Promise.reject(error) |
|
36 |
-); |
|
27 |
+// 모든 클라이언트에 적용될 응답 인터셉터 |
|
28 |
+const responseInterceptor = async error => { |
|
29 |
+ const originalReq = error.config; |
|
30 |
+ if (!error.response) return Promise.reject(error); |
|
37 | 31 |
|
38 |
-// 응답 인터셉터 |
|
39 |
-apiClient.interceptors.response.use( |
|
40 |
- response => { |
|
41 |
- return response |
|
42 |
- }, |
|
43 |
- async error => { |
|
44 |
- const originalReq = error.config; |
|
45 |
- if (!error.response) return Promise.reject(error); |
|
46 |
- |
|
47 |
- // 로그인 요청 |
|
48 |
- if (originalReq?.skipAuthRefresh) { |
|
49 |
- return Promise.reject(error); |
|
50 |
- } |
|
51 |
- |
|
52 |
- // 권한 없음 |
|
53 |
- if (error.response.status === 403 && error.response.data.message === '접근 권한이 없습니다.') { |
|
54 |
- window.history.back(); |
|
55 |
- return Promise.reject(error); |
|
56 |
- } |
|
57 |
- |
|
58 |
- // 리프레시 토큰 요청 |
|
59 |
- if (originalReq.url.includes('/refresh/tknReissue.json')) { |
|
60 |
- return Promise.reject(error); |
|
61 |
- } |
|
62 |
- |
|
63 |
- // 토큰 만료 시 한 번만 재시도 |
|
64 |
- if (error.response.status === 401 && !originalReq._retry) { |
|
65 |
- originalReq._retry = true; |
|
66 |
- try { |
|
67 |
- const res = await axios.post('/refresh/tknReissue.json',{}); |
|
68 |
- const newToken = res.headers.authorization; |
|
69 |
- |
|
70 |
- // 토큰 저장 |
|
71 |
- store.commit('setAuthorization', newToken); |
|
72 |
- originalReq.headers.Authorization = store.state.authorization; |
|
73 |
- |
|
74 |
- // 유저 정보 다시 저장 |
|
75 |
- const user = decodeToken(newToken); |
|
76 |
- store.commit("setUserInfo", { |
|
77 |
- userNm: user.userNm, |
|
78 |
- loginId: user.loginId, |
|
79 |
- userId: user.userId, |
|
80 |
- roles: Array.isArray(user.roles) ? user.roles.map(r => r.authority) : [], |
|
81 |
- }); |
|
82 |
- |
|
83 |
- // 실패했던 요청 재시도 |
|
84 |
- return apiClient(originalReq); |
|
85 |
- } catch (refreshError) { |
|
86 |
- // 리프레시 실패 - 세션 만료 처리 |
|
87 |
- sessionStorage.setItem("redirect", window.location.pathname + window.location.search); |
|
88 |
- alert('세션이 종료 되었습니다.\n로그인을 새로 해주세요.'); |
|
89 |
- store.commit("setStoreReset"); |
|
90 |
- localStorage.clear(); |
|
91 |
- sessionStorage.clear(); |
|
92 |
- window.location.href = '/login.page'; |
|
93 |
- return Promise.reject(refreshError); |
|
94 |
- } |
|
95 |
- } |
|
96 |
- |
|
32 |
+ // 로그인 요청 (skipAuthRefresh 플래그 확인) |
|
33 |
+ if (originalReq?.skipAuthRefresh) { |
|
97 | 34 |
return Promise.reject(error); |
98 | 35 |
} |
99 |
-); |
|
100 | 36 |
|
101 |
-export default apiClient; |
|
37 |
+ // 권한 없음 |
|
38 |
+ if (error.response.status === 403 && error.response.data.message === '접근 권한이 없습니다.') { |
|
39 |
+ alert('접근 권한이 없습니다.'); // 사용자에게 메시지 알림 |
|
40 |
+ window.history.back(); // 뒤로 가기 |
|
41 |
+ return Promise.reject(error); |
|
42 |
+ } |
|
43 |
+ |
|
44 |
+ // 리프레시 토큰 요청 자체는 재시도하지 않음 |
|
45 |
+ if (originalReq.url.includes('/refresh/tknReissue.json')) { |
|
46 |
+ return Promise.reject(error); |
|
47 |
+ } |
|
48 |
+ |
|
49 |
+ // 토큰 만료 시 한 번만 재시도 (401 에러) |
|
50 |
+ if (error.response.status === 401 && !originalReq._retry) { |
|
51 |
+ originalReq._retry = true; // 재시도 플래그 설정 |
|
52 |
+ try { |
|
53 |
+ // 리프레시 토큰으로 새 토큰 발급 요청 |
|
54 |
+ const res = await axios.post('/refresh/tknReissue.json', {}); |
|
55 |
+ const newToken = res.headers.authorization; |
|
56 |
+ |
|
57 |
+ // Vuex 스토어에 새 토큰 저장 |
|
58 |
+ store.commit('setAuthorization', newToken); |
|
59 |
+ originalReq.headers.Authorization = store.state.authorization; // 재시도 요청의 헤더 업데이트 |
|
60 |
+ |
|
61 |
+ // 유저 정보 다시 디코딩하여 스토어에 저장 (새 토큰 기준으로) |
|
62 |
+ const user = decodeToken(newToken); |
|
63 |
+ store.commit("setUserInfo", { |
|
64 |
+ userNm: user.userNm, |
|
65 |
+ loginId: user.loginId, |
|
66 |
+ userId: user.userId, |
|
67 |
+ roles: Array.isArray(user.roles) ? user.roles.map(r => r.authority) : [], |
|
68 |
+ }); |
|
69 |
+ |
|
70 |
+ // 실패했던 원본 요청 재시도 |
|
71 |
+ return axios(originalReq); // axios 인스턴스에 원래 요청 그대로 전달하여 재시도 |
|
72 |
+ } catch (refreshError) { |
|
73 |
+ // 리프레시 실패 시 (세션 만료 등) 로그인 페이지로 강제 이동 |
|
74 |
+ sessionStorage.setItem("redirect", window.location.pathname + window.location.search); |
|
75 |
+ alert('세션이 종료되었습니다.\n로그인을 새로 해주세요.'); |
|
76 |
+ store.commit("setStoreReset"); |
|
77 |
+ localStorage.clear(); |
|
78 |
+ sessionStorage.clear(); |
|
79 |
+ window.location.href = '/login.page'; |
|
80 |
+ return Promise.reject(refreshError); |
|
81 |
+ } |
|
82 |
+ } |
|
83 |
+ |
|
84 |
+ // 그 외 모든 에러는 그대로 reject |
|
85 |
+ return Promise.reject(error); |
|
86 |
+}; |
|
87 |
+ |
|
88 |
+const createConfiguredClient = (contentType) => { |
|
89 |
+ const client = axios.create({ |
|
90 |
+ baseURL: '/', // 모든 API 요청에 기본으로 붙을 URL |
|
91 |
+ headers: { |
|
92 |
+ 'Content-Type': contentType, |
|
93 |
+ }, |
|
94 |
+ }); |
|
95 |
+ |
|
96 |
+ // 요청 및 응답 인터셉터 적용 |
|
97 |
+ client.interceptors.request.use(requestInterceptor, error => Promise.reject(error)); |
|
98 |
+ client.interceptors.response.use(response => response, responseInterceptor); |
|
99 |
+ |
|
100 |
+ return client; |
|
101 |
+}; |
|
102 |
+ |
|
103 |
+// JSON 데이터 요청을 위한 클라이언트 |
|
104 |
+const apiClient = createConfiguredClient('application/json; charset=UTF-8'); |
|
105 |
+ |
|
106 |
+// 멀티파트(파일 업로드) 요청을 위한 클라이언트 |
|
107 |
+const fileClient = createConfiguredClient('multipart/form-data'); |
|
108 |
+ |
|
109 |
+// 모듈 외부로 내보내기 |
|
110 |
+export { apiClient, fileClient };(파일 끝에 줄바꿈 문자 없음) |
--- client/resources/api/login.js
+++ client/resources/api/login.js
... | ... | @@ -1,4 +1,4 @@ |
1 |
-import apiClient from "./index"; |
|
1 |
+import {apiClient} from "./index"; |
|
2 | 2 |
|
3 | 3 |
export const loginProc = userInfo => { |
4 | 4 |
return apiClient.post('/user/login.json', userInfo, {skipAuthRefresh: true}); |
--- client/resources/api/sanctns.js
+++ client/resources/api/sanctns.js
... | ... | @@ -1,4 +1,4 @@ |
1 |
-import apiClient from "./index"; |
|
1 |
+import {apiClient} from "./index"; |
|
2 | 2 |
|
3 | 3 |
// 결재 요청 목록 조회 |
4 | 4 |
export const findMyApprovalRequestsProc = data => { |
--- client/resources/api/user.js
+++ client/resources/api/user.js
... | ... | @@ -1,6 +1,26 @@ |
1 |
-import apiClient from "./index"; |
|
1 |
+import { apiClient, fileClient } from "./index"; |
|
2 |
+import uploadService from "@/resources/js/uploadService"; |
|
2 | 3 |
|
3 | 4 |
// 조회 - 목록 |
4 | 5 |
export const findUsersProc = data => { |
5 | 6 |
return apiClient.get('/user/users.json', { params: data }); |
6 |
-}(파일 끝에 줄바꿈 문자 없음) |
|
7 |
+} |
|
8 |
+ |
|
9 |
+// 스크랩 자료 상세 조회 |
|
10 |
+export const findUsersDetProc = (userId) => { |
|
11 |
+ return apiClient.get(`/user/${userId}/users.json`); |
|
12 |
+} |
|
13 |
+ |
|
14 |
+export const joinProc = (formData) => { |
|
15 |
+ return fileClient.post(`/user/join.file`, formData); |
|
16 |
+} |
|
17 |
+ |
|
18 |
+// 스크랩 자료 수정 (PUT 메서드 사용) |
|
19 |
+export const updateUsersProc = (userId, formData) => { |
|
20 |
+ return fileClient.put(`/user/${userId}/users.file`, formData); |
|
21 |
+} |
|
22 |
+ |
|
23 |
+// 비밀번호 수정 |
|
24 |
+export const updatePassword = (userId, updateUserPasswordDTO) => { |
|
25 |
+ return apiClient.put(`/user/${userId}/updatePassword.json`, updateUserPasswordDTO); |
|
26 |
+} |
--- client/resources/api/vcatn.js
+++ client/resources/api/vcatn.js
... | ... | @@ -1,4 +1,4 @@ |
1 |
-import apiClient from "./index"; |
|
1 |
+import {apiClient} from "./index"; |
|
2 | 2 |
|
3 | 3 |
// 등록 |
4 | 4 |
export const saveVcatnProc = data => { |
+++ client/resources/js/downloadService.js
... | ... | @@ -0,0 +1,257 @@ |
1 | +// @/resources/js/downloadService.js | |
2 | +import { apiClient } from '@/resources/api/index'; | |
3 | +import uploadProgressStore from '@/resources/js/uploadProgressStore'; | |
4 | + | |
5 | +// 파일 다운로드를 처리하는 서비스 | |
6 | +const downloadService = { | |
7 | + // 내부 타이머 변수 | |
8 | + _simulationTimer: null, | |
9 | + | |
10 | + // 마지막 시뮬레이션 진행률 | |
11 | + _lastSimulatedProgress: 0, | |
12 | + | |
13 | + // 진행률 시뮬레이션 시작 (서버 응답이 지연될 때) | |
14 | + _startProgressSimulation() { | |
15 | + // 이미 타이머가 있으면 정리 | |
16 | + if (this._simulationTimer) { | |
17 | + clearInterval(this._simulationTimer); | |
18 | + } | |
19 | + | |
20 | + // 현재 진행률 저장 | |
21 | + this._lastSimulatedProgress = uploadProgressStore.totalProgress; | |
22 | + | |
23 | + // 시작 진행률 설정 (최소 5%) | |
24 | + if (this._lastSimulatedProgress < 5) { | |
25 | + this._lastSimulatedProgress = 5; | |
26 | + uploadProgressStore.totalProgress = 5; | |
27 | + } | |
28 | + | |
29 | + // 진행률 시뮬레이션 시작 (최대 40%까지만) | |
30 | + let simulatedProgress = this._lastSimulatedProgress; | |
31 | + this._simulationTimer = setInterval(() => { | |
32 | + // 실제 진행이 시작되면 시뮬레이션 중단 | |
33 | + if (uploadProgressStore.totalProgress > simulatedProgress) { | |
34 | + this._lastSimulatedProgress = uploadProgressStore.totalProgress; | |
35 | + clearInterval(this._simulationTimer); | |
36 | + this._simulationTimer = null; | |
37 | + return; | |
38 | + } | |
39 | + | |
40 | + // 진행률 증가 (40%까지) | |
41 | + if (simulatedProgress < 40) { | |
42 | + simulatedProgress += Math.random() * 1.5; // 0~1.5% 랜덤하게 증가 | |
43 | + uploadProgressStore.totalProgress = Math.round(simulatedProgress); | |
44 | + this._lastSimulatedProgress = uploadProgressStore.totalProgress; | |
45 | + } | |
46 | + }, 200); // 0.2초마다 업데이트 | |
47 | + }, | |
48 | + | |
49 | + // 시뮬레이션 종료 | |
50 | + _stopProgressSimulation() { | |
51 | + if (this._simulationTimer) { | |
52 | + clearInterval(this._simulationTimer); | |
53 | + this._simulationTimer = null; | |
54 | + } | |
55 | + }, | |
56 | + | |
57 | + // 단일 파일 다운로드 메서드 | |
58 | + async downloadFile(file, options = {}) { | |
59 | + try { | |
60 | + // 다운로드 상태 초기화 (현재 진행률 유지) | |
61 | + const currentProgress = uploadProgressStore.totalProgress; | |
62 | + uploadProgressStore.startDownload(file.fileNm || '파일 다운로드 중...'); | |
63 | + uploadProgressStore.setStage('downloading'); | |
64 | + | |
65 | + // 이전 진행률이 있으면 유지 | |
66 | + if (currentProgress > 0) { | |
67 | + uploadProgressStore.totalProgress = currentProgress; | |
68 | + } | |
69 | + | |
70 | + // 즉시 진행률 시뮬레이션 시작 (서버 응답 전) | |
71 | + this._startProgressSimulation(); | |
72 | + | |
73 | + // apiClient를 사용하여 다운로드 요청 | |
74 | + const response = await apiClient.get(`/file/${file.fileId}/${file.fileOrdr || 1}/fileDownload.json`, { | |
75 | + responseType: 'blob', | |
76 | + ...options, | |
77 | + onDownloadProgress: (progressEvent) => { | |
78 | + // 시뮬레이션 중단 | |
79 | + this._stopProgressSimulation(); | |
80 | + | |
81 | + if (progressEvent.total) { | |
82 | + // 진행 상태 계산 | |
83 | + const realProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total); | |
84 | + | |
85 | + // 실제 진행률이 시뮬레이션 진행률보다 낮으면, 시뮬레이션 진행률부터 시작 | |
86 | + const startProgress = Math.max(this._lastSimulatedProgress, realProgress); | |
87 | + | |
88 | + // 남은 진행률을 60%~100% 범위로 매핑 | |
89 | + // 예: 실제 진행률이 0%일 때 40%, 100%일 때 100%가 되도록 | |
90 | + const adjustedProgress = 40 + (realProgress * 60 / 100); | |
91 | + | |
92 | + // 둘 중 더 큰 값을 사용 | |
93 | + uploadProgressStore.totalProgress = Math.round(Math.max(startProgress, adjustedProgress)); | |
94 | + } | |
95 | + | |
96 | + // 사용자 정의 onDownloadProgress 콜백이 있으면 호출 | |
97 | + if (options.onDownloadProgress) { | |
98 | + options.onDownloadProgress(progressEvent); | |
99 | + } | |
100 | + } | |
101 | + }); | |
102 | + | |
103 | + // 시뮬레이션 중단 (만약 아직 실행 중이라면) | |
104 | + this._stopProgressSimulation(); | |
105 | + | |
106 | + // 다운로드 완료 표시 | |
107 | + uploadProgressStore.totalProgress = 100; | |
108 | + | |
109 | + // 잠시 후 응답 반환 (완료 상태를 보여줄 시간 제공) | |
110 | + await new Promise(resolve => setTimeout(resolve, 300)); | |
111 | + | |
112 | + return response; | |
113 | + } catch (error) { | |
114 | + // 시뮬레이션 중단 | |
115 | + this._stopProgressSimulation(); | |
116 | + | |
117 | + // 오류 발생 시 상태 초기화 | |
118 | + uploadProgressStore.handleError(); | |
119 | + throw error; | |
120 | + } | |
121 | + }, | |
122 | + | |
123 | + // 엑셀 다운로드 메서드 | |
124 | + async downloadExcel(searchReqDTO, fileName, options = {}) { | |
125 | + try { | |
126 | + // 다운로드 상태 초기화 (현재 진행률 유지) | |
127 | + const currentProgress = uploadProgressStore.totalProgress; | |
128 | + uploadProgressStore.startDownload(fileName || '파일 다운로드 중...'); | |
129 | + uploadProgressStore.setStage('downloading'); | |
130 | + | |
131 | + // 이전 진행률이 있으면 유지 | |
132 | + if (currentProgress > 0) { | |
133 | + uploadProgressStore.totalProgress = currentProgress; | |
134 | + } | |
135 | + | |
136 | + // 즉시 진행률 시뮬레이션 시작 (서버 응답 전) | |
137 | + this._startProgressSimulation(); | |
138 | + | |
139 | + // apiClient를 사용하여 다운로드 요청 | |
140 | + const response = await apiClient.get(`/excel/excelDownloadAll.json`, { | |
141 | + params: searchReqDTO, // 검색 조건 파라미터 전달 | |
142 | + responseType: 'blob', // 엑셀 파일을 Blob 형태로 받기 위해 설정 | |
143 | + ...options, // 외부에서 전달된 추가 옵션 적용 | |
144 | + onDownloadProgress: (progressEvent) => { | |
145 | + // 시뮬레이션 중단 | |
146 | + this._stopProgressSimulation(); | |
147 | + | |
148 | + if (progressEvent.total) { | |
149 | + // 진행 상태 계산 | |
150 | + const realProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total); | |
151 | + | |
152 | + // 실제 진행률이 시뮬레이션 진행률보다 낮으면, 시뮬레이션 진행률부터 시작 | |
153 | + const startProgress = Math.max(this._lastSimulatedProgress, realProgress); | |
154 | + | |
155 | + // 남은 진행률을 60%~100% 범위로 매핑 | |
156 | + // 예: 실제 진행률이 0%일 때 40%, 100%일 때 100%가 되도록 | |
157 | + const adjustedProgress = 40 + (realProgress * 60 / 100); | |
158 | + | |
159 | + // 둘 중 더 큰 값을 사용 | |
160 | + uploadProgressStore.totalProgress = Math.round(Math.max(startProgress, adjustedProgress)); | |
161 | + } | |
162 | + | |
163 | + // 사용자 정의 onDownloadProgress 콜백이 있으면 호출 | |
164 | + if (options.onDownloadProgress) { | |
165 | + options.onDownloadProgress(progressEvent); | |
166 | + } | |
167 | + } | |
168 | + }); | |
169 | + | |
170 | + // 시뮬레이션 중단 (만약 아직 실행 중이라면) | |
171 | + this._stopProgressSimulation(); | |
172 | + | |
173 | + // 다운로드 완료 표시 | |
174 | + uploadProgressStore.totalProgress = 100; | |
175 | + | |
176 | + // 잠시 후 응답 반환 (완료 상태를 보여줄 시간 제공) | |
177 | + await new Promise(resolve => setTimeout(resolve, 300)); | |
178 | + | |
179 | + return response; | |
180 | + } catch (error) { | |
181 | + // 시뮬레이션 중단 | |
182 | + this._stopProgressSimulation(); | |
183 | + | |
184 | + // 오류 발생 시 상태 초기화 | |
185 | + uploadProgressStore.handleError(); | |
186 | + throw error; | |
187 | + } | |
188 | + }, | |
189 | + | |
190 | + // 다중 파일 다운로드 메서드 | |
191 | + async downloadMultipleFiles(files, options = {}) { | |
192 | + try { | |
193 | + // 다운로드 상태 초기화 (현재 진행률 유지) | |
194 | + const currentProgress = uploadProgressStore.totalProgress; | |
195 | + uploadProgressStore.startDownload('다중 파일 다운로드 중...'); | |
196 | + uploadProgressStore.setStage('downloading'); | |
197 | + | |
198 | + // 이전 진행률이 있으면 유지 | |
199 | + if (currentProgress > 0) { | |
200 | + uploadProgressStore.totalProgress = currentProgress; | |
201 | + } | |
202 | + | |
203 | + // 즉시 진행률 시뮬레이션 시작 (서버 응답 전) | |
204 | + this._startProgressSimulation(); | |
205 | + | |
206 | + // apiClient를 사용하여 다운로드 요청 | |
207 | + const response = await apiClient.post(`/file/multiFileDownload.json`, files, { | |
208 | + responseType: 'blob', | |
209 | + ...options, | |
210 | + onDownloadProgress: (progressEvent) => { | |
211 | + // 시뮬레이션 중단 | |
212 | + this._stopProgressSimulation(); | |
213 | + | |
214 | + if (progressEvent.total) { | |
215 | + // 진행 상태 계산 | |
216 | + const realProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total); | |
217 | + | |
218 | + // 실제 진행률이 시뮬레이션 진행률보다 낮으면, 시뮬레이션 진행률부터 시작 | |
219 | + const startProgress = Math.max(this._lastSimulatedProgress, realProgress); | |
220 | + | |
221 | + // 남은 진행률을 40%~100% 범위로 매핑 | |
222 | + // 예: 실제 진행률이 0%일 때 40%, 100%일 때 100%가 되도록 | |
223 | + const adjustedProgress = 40 + (realProgress * 60 / 100); | |
224 | + | |
225 | + // 둘 중 더 큰 값을 사용 | |
226 | + uploadProgressStore.totalProgress = Math.round(Math.max(startProgress, adjustedProgress)); | |
227 | + } | |
228 | + | |
229 | + // 사용자 정의 onDownloadProgress 콜백이 있으면 호출 | |
230 | + if (options.onDownloadProgress) { | |
231 | + options.onDownloadProgress(progressEvent); | |
232 | + } | |
233 | + } | |
234 | + }); | |
235 | + | |
236 | + // 시뮬레이션 중단 (만약 아직 실행 중이라면) | |
237 | + this._stopProgressSimulation(); | |
238 | + | |
239 | + // 다운로드 완료 표시 | |
240 | + uploadProgressStore.totalProgress = 100; | |
241 | + | |
242 | + // 잠시 후 응답 반환 (완료 상태를 보여줄 시간 제공) | |
243 | + await new Promise(resolve => setTimeout(resolve, 300)); | |
244 | + | |
245 | + return response; | |
246 | + } catch (error) { | |
247 | + // 시뮬레이션 중단 | |
248 | + this._stopProgressSimulation(); | |
249 | + | |
250 | + // 오류 발생 시 상태 초기화 | |
251 | + uploadProgressStore.handleError(); | |
252 | + throw error; | |
253 | + } | |
254 | + } | |
255 | +}; | |
256 | + | |
257 | +export default downloadService;(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/js/uploadProgressStore.js
... | ... | @@ -0,0 +1,133 @@ |
1 | +import { reactive } from 'vue'; | |
2 | + | |
3 | +// 파일 업로드/다운로드 상태를 관리하는 전역 스토어 | |
4 | +const uploadProgressStore = reactive({ | |
5 | + // 상태 데이터 | |
6 | + isUploading: false, // 모달 표시 여부 | |
7 | + currentFileIndex: 0, | |
8 | + totalFiles: 0, | |
9 | + currentFileName: '', | |
10 | + totalProgress: 0, | |
11 | + stage: '', // 'uploading', 'processing', 'downloading' | |
12 | + processingProgress: 0, // 서버 처리 단계의 가상 진행률 (0-100) | |
13 | + | |
14 | + // 상태 초기화 메서드 | |
15 | + resetState() { | |
16 | + this.isUploading = false; | |
17 | + this.currentFileIndex = 0; | |
18 | + this.totalFiles = 0; | |
19 | + this.currentFileName = ''; | |
20 | + this.totalProgress = 0; | |
21 | + this.stage = ''; | |
22 | + this.processingProgress = 0; | |
23 | + | |
24 | + // 진행 타이머가 있다면 정리 | |
25 | + if (this._processingTimer) { | |
26 | + clearInterval(this._processingTimer); | |
27 | + this._processingTimer = null; | |
28 | + } | |
29 | + }, | |
30 | + | |
31 | + // 업로드 시작 메서드 | |
32 | + startUpload(fileName, totalCount = 1) { | |
33 | + // 혹시 모를 이전 상태 초기화 | |
34 | + this.resetState(); | |
35 | + | |
36 | + // 새 업로드 시작 | |
37 | + this.isUploading = true; | |
38 | + this.currentFileIndex = 1; | |
39 | + this.totalFiles = totalCount; | |
40 | + this.currentFileName = fileName; | |
41 | + this.totalProgress = 0; | |
42 | + this.stage = 'uploading'; | |
43 | + }, | |
44 | + | |
45 | + // 다운로드 시작 메서드 | |
46 | + startDownload(fileName) { | |
47 | + // 혹시 모를 이전 상태 초기화 | |
48 | + this.resetState(); | |
49 | + | |
50 | + // 새 다운로드 시작 | |
51 | + this.isUploading = true; | |
52 | + this.currentFileIndex = 1; | |
53 | + this.totalFiles = 1; | |
54 | + this.currentFileName = fileName; | |
55 | + this.totalProgress = 0; | |
56 | + this.stage = 'downloading'; | |
57 | + }, | |
58 | + | |
59 | + // 진행 상태 업데이트 메서드 | |
60 | + updateProgress(loaded, total) { | |
61 | + if (total > 0) { | |
62 | + const progress = Math.round((loaded * 100) / total); | |
63 | + this.totalProgress = progress; | |
64 | + } | |
65 | + }, | |
66 | + | |
67 | + // 내부용 타이머 변수 | |
68 | + _processingTimer: null, | |
69 | + | |
70 | + // 업로드/다운로드 단계 설정 메서드 | |
71 | + setStage(stage) { | |
72 | + this.stage = stage; | |
73 | + | |
74 | + if (stage === 'uploading' || stage === 'downloading') { | |
75 | + // 업로드/다운로드 단계로 변경 시 처리 단계 타이머 정리 | |
76 | + if (this._processingTimer) { | |
77 | + clearInterval(this._processingTimer); | |
78 | + this._processingTimer = null; | |
79 | + } | |
80 | + this.processingProgress = 0; | |
81 | + } | |
82 | + else if (stage === 'processing') { | |
83 | + // 파일 업로드는 100% 완료 | |
84 | + this.totalProgress = 100; | |
85 | + | |
86 | + // 처리 단계 진행 시뮬레이션 시작 | |
87 | + this.processingProgress = 0; | |
88 | + | |
89 | + // 타이머가 이미 있으면 정리 | |
90 | + if (this._processingTimer) { | |
91 | + clearInterval(this._processingTimer); | |
92 | + } | |
93 | + | |
94 | + // 처리 단계 진행 시뮬레이션을 위한 타이머 설정 | |
95 | + // 최대 95%까지만 채워서 실제 완료 전에는 100%가 되지 않도록 함 | |
96 | + this._processingTimer = setInterval(() => { | |
97 | + if (this.processingProgress < 95) { | |
98 | + // 처음에는 빠르게, 나중에는 천천히 증가하도록 설정 | |
99 | + const increment = Math.max(1, 10 - Math.floor(this.processingProgress / 10)); | |
100 | + this.processingProgress += increment; | |
101 | + } else { | |
102 | + // 95%에 도달하면 타이머 정지 | |
103 | + clearInterval(this._processingTimer); | |
104 | + this._processingTimer = null; | |
105 | + } | |
106 | + }, 500); // 0.5초마다 업데이트 | |
107 | + } | |
108 | + }, | |
109 | + | |
110 | + // 서버 처리 완료 메서드 (API 응답 수신 후 호출) | |
111 | + completeProcessing() { | |
112 | + // 처리 타이머 정리 | |
113 | + if (this._processingTimer) { | |
114 | + clearInterval(this._processingTimer); | |
115 | + this._processingTimer = null; | |
116 | + } | |
117 | + | |
118 | + // 처리 진행률 100%로 설정 | |
119 | + this.processingProgress = 100; | |
120 | + }, | |
121 | + | |
122 | + // 모달 닫기 메서드 (alert 표시 후 호출) | |
123 | + closeModal() { | |
124 | + this.resetState(); | |
125 | + }, | |
126 | + | |
127 | + // 오류 발생 시 호출할 메서드 | |
128 | + handleError() { | |
129 | + this.resetState(); | |
130 | + } | |
131 | +}); | |
132 | + | |
133 | +export default uploadProgressStore;(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/js/uploadService.js
... | ... | @@ -0,0 +1,109 @@ |
1 | +import { fileClient } from '@/resources/api/index'; | |
2 | +import uploadProgressStore from '@/resources/js/uploadProgressStore'; | |
3 | + | |
4 | +// 파일 업로드를 처리하는 서비스 | |
5 | +const uploadService = { | |
6 | + // POST 메서드를 사용한 업로드 (등록) | |
7 | + async uploadWithPost(url, formData, options = {}) { | |
8 | + // 파일 정보 추출 | |
9 | + const file = formData.get('multipartFiles') || formData.get('file'); | |
10 | + | |
11 | + if (file) { | |
12 | + // 업로드 상태 초기화 및 파일 전송 단계 시작 (이 초기화는 유지) | |
13 | + uploadProgressStore.startUpload(file.name, 1); | |
14 | + } | |
15 | + | |
16 | + try { | |
17 | + // 1단계: 파일 전송 단계 | |
18 | + uploadProgressStore.setStage('uploading'); | |
19 | + | |
20 | + // fileClient.post를 사용하여 업로드 요청 | |
21 | + const response = await fileClient.post(url, formData, { | |
22 | + ...options, | |
23 | + onUploadProgress: (progressEvent) => { | |
24 | + // 진행 상태 업데이트 | |
25 | + uploadProgressStore.updateProgress(progressEvent.loaded, progressEvent.total); | |
26 | + | |
27 | + // 사용자 정의 onUploadProgress 콜백이 있으면 호출 | |
28 | + if (options.onUploadProgress) { | |
29 | + options.onUploadProgress(progressEvent); | |
30 | + } | |
31 | + | |
32 | + // 업로드가 완료되면 처리 중 단계로 전환 | |
33 | + if (progressEvent.loaded === progressEvent.total) { | |
34 | + // setTimeout을 사용하여 처리 단계 전환을 강제로 비동기 실행 | |
35 | + setTimeout(() => { | |
36 | + uploadProgressStore.setStage('processing'); | |
37 | + }, 100); | |
38 | + } | |
39 | + } | |
40 | + }); | |
41 | + | |
42 | + // 서버 응답 수신 - 처리 완료 표시 | |
43 | + uploadProgressStore.completeProcessing(); | |
44 | + | |
45 | + // 잠시 후 응답 반환 (처리 완료 상태를 보여줄 시간 제공) | |
46 | + await new Promise(resolve => setTimeout(resolve, 500)); | |
47 | + | |
48 | + // 응답 반환 (alert은 API 함수에서 처리) | |
49 | + return response; | |
50 | + } catch (error) { | |
51 | + // 오류 발생 시 상태 초기화 | |
52 | + uploadProgressStore.handleError(); | |
53 | + throw error; // 오류 다시 throw | |
54 | + } | |
55 | + }, | |
56 | + | |
57 | + // PUT 메서드를 사용한 업로드 (수정) | |
58 | + async uploadWithPut(url, formData, options = {}) { | |
59 | + // 파일 정보 추출 | |
60 | + const file = formData.get('multipartFiles') || formData.get('file'); | |
61 | + | |
62 | + if (file) { | |
63 | + // 업로드 상태 초기화 및 파일 전송 단계 시작 (이 초기화는 유지) | |
64 | + uploadProgressStore.startUpload(file.name, 1); | |
65 | + } | |
66 | + | |
67 | + try { | |
68 | + // 1단계: 파일 전송 단계 | |
69 | + uploadProgressStore.setStage('uploading'); | |
70 | + | |
71 | + // fileClient.put을 사용하여 업로드 요청 | |
72 | + const response = await fileClient.put(url, formData, { | |
73 | + ...options, | |
74 | + onUploadProgress: (progressEvent) => { | |
75 | + // 진행 상태 업데이트 | |
76 | + uploadProgressStore.updateProgress(progressEvent.loaded, progressEvent.total); | |
77 | + | |
78 | + // 사용자 정의 onUploadProgress 콜백이 있으면 호출 | |
79 | + if (options.onUploadProgress) { | |
80 | + options.onUploadProgress(progressEvent); | |
81 | + } | |
82 | + | |
83 | + // 업로드가 완료되면 처리 중 단계로 전환 | |
84 | + if (progressEvent.loaded === progressEvent.total) { | |
85 | + // setTimeout을 사용하여 처리 단계 전환을 강제로 비동기 실행 | |
86 | + setTimeout(() => { | |
87 | + uploadProgressStore.setStage('processing'); | |
88 | + }, 100); | |
89 | + } | |
90 | + } | |
91 | + }); | |
92 | + | |
93 | + // 서버 응답 수신 - 처리 완료 표시 | |
94 | + uploadProgressStore.completeProcessing(); | |
95 | + | |
96 | + // 잠시 후 응답 반환 (처리 완료 상태를 보여줄 시간 제공) | |
97 | + await new Promise(resolve => setTimeout(resolve, 500)); | |
98 | + | |
99 | + // 응답 반환 (alert은 API 함수에서 처리) | |
100 | + return response; | |
101 | + } catch (error) { | |
102 | + // 오류 발생 시 상태 초기화 | |
103 | + uploadProgressStore.handleError(); | |
104 | + throw error; // 오류 다시 throw | |
105 | + } | |
106 | + } | |
107 | +}; | |
108 | + | |
109 | +export default uploadService;(파일 끝에 줄바꿈 문자 없음) |
+++ client/views/component/FileUploadProgress.vue
... | ... | @@ -0,0 +1,267 @@ |
1 | +// @/components/common/FileUploadProgress.vue <template> | |
2 | + <transition name="fade"> | |
3 | + <div v-if="progress.isUploading" class="upload-modal-overlay"> | |
4 | + <div class="upload-modal"> | |
5 | + <div class="upload-modal-content"> | |
6 | + <div class="upload-icon"> | |
7 | + <!-- 파일 업로드 중 아이콘 --> | |
8 | + <svg v-if="currentStage === 'uploading'" width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
9 | + <path d="M12 16V8M12 8L9 11M12 8L15 11" stroke="#4285F4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> | |
10 | + <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" /> | |
11 | + </svg> | |
12 | + <!-- 파일 다운로드 중 아이콘 --> | |
13 | + <svg v-else-if="currentStage === 'downloading'" width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
14 | + <path d="M12 8V16M12 16L9 13M12 16L15 13" stroke="#4285F4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /> | |
15 | + <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" /> | |
16 | + </svg> | |
17 | + <!-- 서버 처리 중 스피너 아이콘 --> | |
18 | + <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"> | |
19 | + <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" /> | |
20 | + <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" /> | |
21 | + </svg> | |
22 | + </div> | |
23 | + <!-- 단계별 제목 --> | |
24 | + <h3> | |
25 | + <span v-if="currentStage === 'uploading'">파일 업로드 진행 중</span> | |
26 | + <span v-else-if="currentStage === 'downloading'">파일 다운로드 진행 중</span> | |
27 | + <span v-else-if="currentStage === 'processing'">서버 처리 중...</span> | |
28 | + </h3> | |
29 | + <!-- 파일 정보 --> | |
30 | + <div class="file-info"> | |
31 | + <span class="file-name">{{ progress.currentFileName }}</span> | |
32 | + <span class="file-count">{{ progress.currentFileIndex }}/{{ progress.totalFiles }}</span> | |
33 | + </div> | |
34 | + <!-- 진행 바 (업로드/다운로드 중 또는 서버 처리 중에 따라 다른 진행 바 표시) --> | |
35 | + <div v-if="currentStage === 'uploading' || currentStage === 'downloading'" class="progress-bar-container"> | |
36 | + <div class="progress-bar-fill" :class="{ | |
37 | + 'downloading': currentStage === 'downloading', | |
38 | + 'simulated': isSimulated | |
39 | + }" :style="{ width: progress.totalProgress + '%' }"></div> | |
40 | + </div> | |
41 | + <div v-else-if="currentStage === 'processing'" class="progress-bar-container"> | |
42 | + <!-- 서버 처리 중일 때는 processingProgress 사용 --> | |
43 | + <div class="progress-bar-fill processing" :style="{ width: progress.processingProgress + '%' }"></div> | |
44 | + </div> | |
45 | + <!-- 진행 상태 메시지 --> | |
46 | + <div class="progress-percentage"> | |
47 | + <span v-if="currentStage === 'uploading'">{{ progress.totalProgress }}% 완료</span> | |
48 | + <span v-else-if="currentStage === 'downloading'"> | |
49 | + <template v-if="progress.totalProgress <= 5">다운로드 요청 중...</template> | |
50 | + <template v-else-if="progress.totalProgress < 40 && isSimulated">다운로드 준비 중... {{ progress.totalProgress }}%</template> | |
51 | + <template v-else-if="progress.totalProgress < 40">다운로드 준비 완료</template> | |
52 | + <template v-else>{{ progress.totalProgress }}% 완료</template> | |
53 | + </span> | |
54 | + <span v-else-if="currentStage === 'processing' && progress.processingProgress < 100"> 처리 중... {{ progress.processingProgress }}% </span> | |
55 | + <span v-else-if="currentStage === 'processing' && progress.processingProgress === 100"> 처리 완료! </span> | |
56 | + </div> | |
57 | + </div> | |
58 | + </div> | |
59 | + </div> | |
60 | + </transition> | |
61 | +</template> | |
62 | +<script> | |
63 | +import uploadProgressStore from '@/resources/js/uploadProgressStore'; | |
64 | + | |
65 | +export default { | |
66 | + name: 'FileUploadProgress', | |
67 | + | |
68 | + data() { | |
69 | + return { | |
70 | + progress: uploadProgressStore, | |
71 | + currentStage: 'uploading', | |
72 | + isSimulated: false, | |
73 | + // 진행률 변화 추적을 위한 변수 | |
74 | + lastProgress: 0, | |
75 | + lastProgressUpdateTime: 0, | |
76 | + progressCheckTimer: null | |
77 | + }; | |
78 | + }, | |
79 | + | |
80 | + created() { | |
81 | + // 초기 상태 설정 | |
82 | + this.currentStage = this.progress.stage || 'uploading'; | |
83 | + this.lastProgressUpdateTime = Date.now(); | |
84 | + | |
85 | + // 진행률 변화 감지 타이머 설정 | |
86 | + this.progressCheckTimer = setInterval(() => { | |
87 | + const now = Date.now(); | |
88 | + | |
89 | + // 진행률이 정체되어 있으면 시뮬레이션 중으로 간주 | |
90 | + if (this.progress.totalProgress === this.lastProgress) { | |
91 | + // 1초 이상 정체된 경우에만 시뮬레이션으로 판단 | |
92 | + if (now - this.lastProgressUpdateTime > 1000) { | |
93 | + this.isSimulated = true; | |
94 | + } | |
95 | + } else { | |
96 | + // 진행률이 변경되면 타임스탬프 업데이트 | |
97 | + this.lastProgressUpdateTime = now; | |
98 | + this.isSimulated = false; | |
99 | + } | |
100 | + | |
101 | + this.lastProgress = this.progress.totalProgress; | |
102 | + }, 500); | |
103 | + }, | |
104 | + | |
105 | + beforeDestroy() { | |
106 | + // 타이머 정리 | |
107 | + if (this.progressCheckTimer) { | |
108 | + clearInterval(this.progressCheckTimer); | |
109 | + } | |
110 | + }, | |
111 | + | |
112 | + watch: { | |
113 | + // progress.stage 변경 감시 | |
114 | + 'progress.stage'(newStage) { | |
115 | + this.currentStage = newStage; | |
116 | + }, | |
117 | + | |
118 | + // 진행률 변경 감시 | |
119 | + 'progress.totalProgress'(newValue, oldValue) { | |
120 | + if (newValue !== oldValue) { | |
121 | + this.lastProgressUpdateTime = Date.now(); | |
122 | + | |
123 | + // 진행률이 크게 변경된 경우 (실제 진행률 업데이트로 간주) | |
124 | + if (Math.abs(newValue - oldValue) > 5) { | |
125 | + this.isSimulated = false; | |
126 | + } | |
127 | + } | |
128 | + } | |
129 | + } | |
130 | +} | |
131 | +</script> | |
132 | +<style scoped> | |
133 | +.fade-enter-active, | |
134 | +.fade-leave-active { | |
135 | + transition: opacity 0.3s; | |
136 | +} | |
137 | + | |
138 | +.fade-enter-from, | |
139 | +.fade-leave-to { | |
140 | + opacity: 0; | |
141 | +} | |
142 | + | |
143 | +.upload-modal-overlay { | |
144 | + position: fixed; | |
145 | + top: 0; | |
146 | + left: 0; | |
147 | + width: 100%; | |
148 | + height: 100%; | |
149 | + background-color: rgba(0, 0, 0, 0.5); | |
150 | + display: flex; | |
151 | + justify-content: center; | |
152 | + align-items: center; | |
153 | + z-index: 9999; | |
154 | +} | |
155 | + | |
156 | +.upload-modal { | |
157 | + background-color: white; | |
158 | + border-radius: 8px; | |
159 | + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | |
160 | + width: 360px; | |
161 | + padding: 0; | |
162 | + overflow: hidden; | |
163 | +} | |
164 | + | |
165 | +.upload-modal-content { | |
166 | + padding: 24px; | |
167 | + display: flex; | |
168 | + flex-direction: column; | |
169 | + align-items: center; | |
170 | +} | |
171 | + | |
172 | +.upload-icon { | |
173 | + margin-bottom: 16px; | |
174 | +} | |
175 | + | |
176 | +/* 스피너 애니메이션 */ | |
177 | +.spinner { | |
178 | + animation: spin 1.5s linear infinite; | |
179 | +} | |
180 | + | |
181 | +@keyframes spin { | |
182 | + 0% { | |
183 | + transform: rotate(0deg); | |
184 | + } | |
185 | + | |
186 | + 100% { | |
187 | + transform: rotate(360deg); | |
188 | + } | |
189 | +} | |
190 | + | |
191 | +h3 { | |
192 | + font-size: 18px; | |
193 | + font-weight: 600; | |
194 | + margin: 0 0 16px 0; | |
195 | + color: #333; | |
196 | +} | |
197 | + | |
198 | +.file-info { | |
199 | + width: 100%; | |
200 | + margin-bottom: 12px; | |
201 | + display: flex; | |
202 | + justify-content: space-between; | |
203 | + font-size: 14px; | |
204 | +} | |
205 | + | |
206 | +.file-name { | |
207 | + color: #555; | |
208 | + max-width: 240px; | |
209 | + white-space: nowrap; | |
210 | + overflow: hidden; | |
211 | + text-overflow: ellipsis; | |
212 | +} | |
213 | + | |
214 | +.file-count { | |
215 | + color: #777; | |
216 | +} | |
217 | + | |
218 | +.progress-bar-container { | |
219 | + width: 100%; | |
220 | + height: 8px; | |
221 | + background-color: #f0f0f0; | |
222 | + border-radius: 4px; | |
223 | + overflow: hidden; | |
224 | + margin-bottom: 8px; | |
225 | +} | |
226 | + | |
227 | +.progress-bar-fill { | |
228 | + height: 100%; | |
229 | + background-color: #4285F4; | |
230 | + transition: width 0.3s ease; | |
231 | +} | |
232 | + | |
233 | +/* 다운로드 중일 때 진행 바 스타일 */ | |
234 | +.progress-bar-fill.downloading { | |
235 | + background-color: #34A853; | |
236 | +} | |
237 | + | |
238 | +/* 시뮬레이션 중일 때 진행 바 스타일 */ | |
239 | +.progress-bar-fill.simulated { | |
240 | + background: linear-gradient(90deg, #34A853 25%, #66BB6A 50%, #34A853 75%); | |
241 | + background-size: 200% 100%; | |
242 | + animation: loading 2s infinite linear; | |
243 | +} | |
244 | + | |
245 | +/* 서버 처리 중일 때 진행 바 스타일 */ | |
246 | +.progress-bar-fill.processing { | |
247 | + background: linear-gradient(90deg, #4CAF50 25%, #81C784 50%, #4CAF50 75%); | |
248 | + background-size: 200% 100%; | |
249 | + animation: loading 1.5s infinite linear; | |
250 | +} | |
251 | + | |
252 | +@keyframes loading { | |
253 | + 0% { | |
254 | + background-position: 100% 50%; | |
255 | + } | |
256 | + | |
257 | + 100% { | |
258 | + background-position: 0% 50%; | |
259 | + } | |
260 | +} | |
261 | + | |
262 | +.progress-percentage { | |
263 | + font-size: 14px; | |
264 | + color: #555; | |
265 | + font-weight: 500; | |
266 | +} | |
267 | +</style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/layout/AdminHeader.vue
+++ client/views/layout/AdminHeader.vue
... | ... | @@ -18,9 +18,9 @@ |
18 | 18 |
}; |
19 | 19 |
}, |
20 | 20 |
methods: { |
21 |
- logout(){ |
|
21 |
+ logout() { |
|
22 | 22 |
localStorage.removeItem('isLoggedIn'); |
23 |
-this.$router.push('/login.page'); |
|
23 |
+ this.$router.push('/login.page'); |
|
24 | 24 |
|
25 | 25 |
} |
26 | 26 |
}, |
... | ... | @@ -29,9 +29,9 @@ |
29 | 29 |
components: { |
30 | 30 |
'Menu': Menu, |
31 | 31 |
}, |
32 |
- created() {}, |
|
33 |
- mounted() {}, |
|
34 |
- beforeUnmount() {}, |
|
32 |
+ created() { }, |
|
33 |
+ mounted() { }, |
|
34 |
+ beforeUnmount() { }, |
|
35 | 35 |
}; |
36 | 36 |
|
37 | 37 |
</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/Manager/asset/CardInfoManagement.vue
+++ client/views/pages/Manager/asset/CardInfoManagement.vue
... | ... | @@ -167,7 +167,7 @@ |
167 | 167 |
useAll: false, |
168 | 168 |
listType:"all", |
169 | 169 |
|
170 |
- currentPage: null, |
|
170 |
+ currentPage: 1, |
|
171 | 171 |
recordSize: 4, |
172 | 172 |
pageSize: null, |
173 | 173 |
totalRecordCount: null, |
--- client/views/pages/Manager/hr/hrDetail.vue
+++ client/views/pages/Manager/hr/hrDetail.vue
... | ... | @@ -2,108 +2,91 @@ |
2 | 2 |
<div class="card "> |
3 | 3 |
<div class="card-body "> |
4 | 4 |
<h2 class="card-title">직원 검색</h2> |
5 |
- |
|
6 |
- |
|
7 |
- <div class="name-box flex simple"> |
|
8 |
- <div class="img-area" > |
|
9 |
- <div class="img"><img :src="photoicon" alt=""> |
|
10 |
- </div> |
|
11 |
- </div> |
|
12 |
- <form class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }" |
|
13 |
- @submit.prevent="handleRegister" novalidate > |
|
14 |
- <div class="col-12"> |
|
15 |
- <label for="yourName" class="form-label">아이디</label> |
|
16 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly |
|
17 |
- placeholder="admin"> |
|
18 |
- </div> |
|
19 |
- <div class="col-12 "> |
|
20 |
- <div class="col-12 border-x"> |
|
21 |
- <label for="youremail" class="form-label ">이름<p class="require"><img :src="require" alt=""></p></label> |
|
22 |
- <input v-model="email" type="text" name="username" class="form-control" id="youremail" required |
|
23 |
- readonly> |
|
24 |
- </div> |
|
25 |
- |
|
26 |
- <div class="col-12 border-x"> |
|
27 |
- <label for="yourPassword" class="form-label">부서</label> |
|
28 |
- <input v-model="password" type="password" name="password" class="form-control" id="yourPassword" |
|
29 |
- required readonly placeholder="주식회사 테이큰 소프트"> |
|
30 |
- </div> |
|
31 |
- </div> |
|
32 |
- <div class="col-12 border-x"> |
|
33 |
- <div class="col-12 border-x"> |
|
34 |
- <label for="youremail" class="form-label">직급</label> |
|
35 |
- <input v-model="email" type="text" name="username" class="form-control" id="youremail" required readonly |
|
36 |
- placeholder="과장"> |
|
37 |
- </div> |
|
38 |
- |
|
39 |
- <div class="col-12 border-x"> |
|
40 |
- <label for="yourPassword" class="form-label">직책</label> |
|
41 |
- <input v-model="password" type="password" name="password" class="form-control" id="yourPassword" |
|
42 |
- required readonly placeholder="팀장"> |
|
43 |
- </div> |
|
44 |
- </div> |
|
45 |
- |
|
46 |
- |
|
47 |
- </form> |
|
48 |
- |
|
5 |
+ <div class="name-box flex simple"> |
|
6 |
+ <div class="img-area"> |
|
7 |
+ <div class="img"><img :src="selectedUser.thumbnail?.filePath || photoicon" alt=""> |
|
49 | 8 |
</div> |
50 |
- <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" |
|
51 |
- @submit.prevent="handleRegister" novalidate style="margin-bottom: 20px;"> |
|
52 |
- <div class="col-12"> |
|
53 |
- <label for="yourName" class="form-label">연락처</label> |
|
54 |
- <input v-model="name" type="text" name="name" class="form-control " id="yourName" required readonly |
|
55 |
- placeholder="admin"> |
|
56 |
- </div> |
|
57 |
- <div class="col-12"> |
|
58 |
- <label for="yourName" class="form-label">생년월일</label> |
|
59 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly |
|
60 |
- placeholder="admin"> |
|
9 |
+ </div> |
|
10 |
+ <div class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }" |
|
11 |
+ @submit.prevent="handleRegister" novalidate> |
|
12 |
+ <div class="col-12"> |
|
13 |
+ <label for="loginId" class="form-label">아이디</label> |
|
14 |
+ <input v-model="selectedUser.loginId" type="text" name="name" class="form-control" id="yourName" required |
|
15 |
+ readonly placeholder="admin"> |
|
16 |
+ </div> |
|
17 |
+ <div class="col-12 "> |
|
18 |
+ <div class="col-12 border-x"> |
|
19 |
+ <label for="userNm" class="form-label ">이름<p class="require"><img :src="require" alt=""></p></label> |
|
20 |
+ <input v-model="selectedUser.userNm" type="text" name="username" class="form-control" id="youremail" |
|
21 |
+ required readonly> |
|
61 | 22 |
</div> |
62 | 23 |
<div class="col-12 border-x"> |
63 |
- <label for="yourName" class="form-label">입사일</label> |
|
64 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly |
|
65 |
- placeholder="admin"> |
|
24 |
+ <label for="deptNm" class="form-label">부서</label> |
|
25 |
+ <input v-model="selectedUser.deptNm" type="text" name="password" class="form-control" id="yourPassword" |
|
26 |
+ required readonly placeholder="주식회사 테이큰 소프트"> |
|
66 | 27 |
</div> |
67 |
- |
|
68 |
- |
|
69 |
- |
|
70 |
- </form> |
|
71 |
- <form class="row g-3 needs-validation detail salary" :class="{ 'was-validated': formSubmitted }" |
|
72 |
- @submit.prevent="handleRegister" novalidate> |
|
73 |
- <div class=" col-12 border-x"><label>연봉</label> |
|
74 |
- <div class="yearsalary"> |
|
75 |
- <div class="col-12"> |
|
76 |
- <label for="" class="second-label">2023</label> |
|
77 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly |
|
78 |
- placeholder="admin"> |
|
79 |
- </div> |
|
80 |
- <div class="col-12"> |
|
81 |
- <label for="" class="second-label">2023</label> |
|
82 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly |
|
83 |
- placeholder="admin"> |
|
84 |
- </div> |
|
85 |
- <div class="col-12 border-x"> |
|
86 |
- <label for="" class="second-label">2023</label> |
|
87 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly |
|
88 |
- placeholder="admin"> |
|
89 |
- </div> |
|
28 |
+ </div> |
|
29 |
+ <div class="col-12 border-x"> |
|
30 |
+ <div class="col-12 border-x"> |
|
31 |
+ <label for="clsfNm" class="form-label">직급</label> |
|
32 |
+ <input v-model="selectedUser.clsfNm" type="text" name="username" class="form-control" id="youremail" |
|
33 |
+ required readonly> |
|
90 | 34 |
</div> |
35 |
+ <div class="col-12 border-x"> |
|
36 |
+ <label for="rspofcNm" class="form-label">직책</label> |
|
37 |
+ <input v-model="selectedUser.rspofcNm" type="password" name="password" class="form-control" |
|
38 |
+ id="yourPassword" required readonly> |
|
91 | 39 |
</div> |
92 |
- </form> |
|
93 |
- <div class="buttons"> |
|
94 |
- <button type="delete" class="btn sm sm btn-red">탈퇴</button> |
|
95 |
- <button type="reset" class="btn sm sm secondary">수정</button> |
|
96 |
- <button type="submit" class="btn sm sm tertiary">목록</button> |
|
40 |
+ </div> |
|
41 |
+ </div> |
|
42 |
+ </div> |
|
43 |
+ <div class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" |
|
44 |
+ @submit.prevent="handleRegister" novalidate style="margin-bottom: 20px;"> |
|
45 |
+ <div class="col-12"> |
|
46 |
+ <label for="mbtlnum" class="form-label">연락처</label> |
|
47 |
+ <input v-model="selectedUser.mbtlnum" type="text" name="name" class="form-control " id="yourName" required |
|
48 |
+ readonly placeholder="admin"> |
|
49 |
+ </div> |
|
50 |
+ <div class="col-12"> |
|
51 |
+ <label for="brthdy" class="form-label">생년월일</label> |
|
52 |
+ <input v-model="selectedUser.brthdy" type="text" name="name" class="form-control" id="yourName" required |
|
53 |
+ readonly placeholder="admin"> |
|
54 |
+ </div> |
|
55 |
+ <div class="col-12 border-x"> |
|
56 |
+ <label for="encpn" class="form-label">입사일</label> |
|
57 |
+ <input v-model="selectedUser.encpn" type="text" name="name" class="form-control" id="yourName" required |
|
58 |
+ readonly placeholder="admin"> |
|
59 |
+ </div> |
|
60 |
+ </div> |
|
61 |
+ <div class="row g-3 needs-validation detail salary" :class="{ 'was-validated': formSubmitted }" |
|
62 |
+ @submit.prevent="handleRegister" novalidate> |
|
63 |
+ <div class=" col-12 border-x"><label>연봉</label> |
|
64 |
+ <div class="yearsalary"> |
|
65 |
+ <div class="col-12" v-for="(salary, index) in selectedUser.salaryList" :key="index"> |
|
66 |
+ <label for="year" class="second-label">{{ salary.year }}</label> |
|
67 |
+ <input v-model="salary.amount" type="text" name="amount" class="form-control" id="amount" required |
|
68 |
+ readonly placeholder="admin" /> |
|
69 |
+ </div> |
|
70 |
+ </div> |
|
71 |
+ </div> |
|
72 |
+ </div> |
|
73 |
+ <div class="buttons"> |
|
74 |
+ <button type="delete" class="btn sm sm btn-red" @click="submitDeleteForm" |
|
75 |
+ v-if="selectedUser.useAt === 'Y'">탈퇴</button> |
|
76 |
+ <button type="delete" class="btn sm sm btn-red" @click="submitRepairForm" |
|
77 |
+ v-if="selectedUser.useAt === 'N'">복구</button> |
|
78 |
+ <button type="reset" class="btn sm sm secondary" @click="goToDetail" |
|
79 |
+ v-if="selectedUser.useAt === 'Y'">수정</button> |
|
80 |
+ <button type="submit" class="btn sm sm tertiary" @click="goToPage">목록</button> |
|
97 | 81 |
</div> |
98 | 82 |
</div> |
99 |
- |
|
100 | 83 |
</div> |
101 |
- |
|
102 | 84 |
</template> |
103 | 85 |
|
104 | 86 |
<script> |
105 | 87 |
import GoogleCalendar from "../../../component/GoogleCalendar.vue" |
106 | 88 |
import { SearchOutlined } from '@ant-design/icons-vue'; |
89 |
+import { findUsersDetProc, updateUsersProc } from "../../../../resources/api/user" |
|
107 | 90 |
export default { |
108 | 91 |
data() { |
109 | 92 |
return { |
... | ... | @@ -114,61 +97,107 @@ |
114 | 97 |
startbtn: "/client/resources/img/start.png", |
115 | 98 |
stopbtn: "/client/resources/img/stop.png", |
116 | 99 |
moreicon: "/client/resources/img/more.png", |
117 |
- today: new Date().toLocaleDateString('ko-KR', { |
|
118 |
- year: 'numeric', |
|
119 |
- month: '2-digit', |
|
120 |
- day: '2-digit', |
|
121 |
- weekday: 'short', |
|
122 |
- }), |
|
123 |
- time: this.getCurrentTime(), |
|
124 |
- listData: Array.from({ length: 20 }, (_, i) => ({ |
|
125 |
- department: `부서 ${i + 1}`, |
|
126 |
- name: `이름 ${i + 1}`, |
|
127 |
- position: `직급 ${i + 1}` |
|
128 |
- })) |
|
100 |
+ selectedUser: {}, |
|
129 | 101 |
} |
130 | 102 |
}, |
131 | 103 |
components: { |
132 |
- SearchOutlined |
|
104 |
+ SearchOutlined, findUsersDetProc |
|
133 | 105 |
}, |
134 | 106 |
methods: { |
135 |
- formatBudget(amount) { |
|
136 |
- return new Intl.NumberFormat().format(amount) + ' 원'; |
|
137 |
- }, |
|
138 |
- isPastPeriod(period) { |
|
139 |
- // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출 |
|
140 |
- const endDateStr = period.split('~')[1]?.trim(); |
|
141 |
- if (!endDateStr) return false; |
|
107 |
+ //사용자 정보 상세 조회 |
|
108 |
+ async searchUser() { |
|
109 |
+ try { |
|
110 |
+ const response = await findUsersDetProc(this.$route.query.id); |
|
111 |
+ if (response.status === 200) { |
|
112 |
+ this.selectedUser = response.data.data.user; // API 응답에서 카테고리 목록을 가져옴 |
|
142 | 113 |
|
143 |
- const endDate = new Date(endDateStr); |
|
144 |
- const today = new Date(); |
|
114 |
+ console.log("유저 아이디", this.selectedUser); |
|
115 |
+ } |
|
116 |
+ } catch (error) { |
|
117 |
+ console.error("검색 중 오류 발생:", error); |
|
118 |
+ } |
|
119 |
+ }, |
|
120 |
+ goToPage() { |
|
121 |
+ this.$router.push({ name: 'hrManagement' }); |
|
122 |
+ }, |
|
123 |
+ goToDetail() { |
|
124 |
+ this.$router.push({ |
|
125 |
+ name: 'hrInsert', |
|
126 |
+ query: { id: this.selectedUser.userId } |
|
127 |
+ }); |
|
128 |
+ }, |
|
129 |
+ async submitDeleteForm() { |
|
130 |
+ const confirmDelete = confirm("정말로 사용자를 탈퇴시키겠습니까?\n이 작업은 되돌릴 수 없습니다."); |
|
145 | 131 |
|
146 |
- // 현재 날짜보다 과거면 true |
|
147 |
- return endDate < today; |
|
148 |
- }, |
|
149 |
- getStatusClass(status) { |
|
150 |
- return status === 'active' ? 'status-active' : 'status-inactive'; |
|
151 |
- }, |
|
152 |
- getStatusClass(status) { |
|
153 |
- if (status === '미진행') return 'status-pending'; |
|
154 |
- if (status === '진행중') return 'status-approved'; |
|
132 |
+ if (!confirmDelete) { // 사용자가 '아니오'를 눌렀을 때 |
|
133 |
+ alert("사용자 탈퇴가 취소되었습니다."); |
|
134 |
+ return; // 함수 실행 중단 |
|
135 |
+ } |
|
155 | 136 |
|
156 |
- // Default empty string |
|
157 |
- return ''; |
|
137 |
+ // 2. FormData 객체 생성 |
|
138 |
+ const formData = new FormData(); |
|
139 |
+ |
|
140 |
+ // 수정용 DTO 생성 (기존 데이터 + 수정 데이터) |
|
141 |
+ const updateDTO = { |
|
142 |
+ userNm: this.selectedUser.userNm, |
|
143 |
+ useAt: "N", |
|
144 |
+ fileId: this.selectedUser.fileId, |
|
145 |
+ }; |
|
146 |
+ |
|
147 |
+ formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], { |
|
148 |
+ type: 'application/json' |
|
149 |
+ })); |
|
150 |
+ |
|
151 |
+ // 4. API 통신 |
|
152 |
+ try { |
|
153 |
+ const response = await updateUsersProc(this.$route.query.id, formData); |
|
154 |
+ |
|
155 |
+ if (response.status === 200) { |
|
156 |
+ alert('사용자 탈퇴퇴가 성공적으로 저장되었습니다!'); |
|
157 |
+ } |
|
158 |
+ } catch (error) { |
|
159 |
+ console.error("사용자 정보 저장 중 오류 발생:", error); |
|
160 |
+ alert("사용자 정보 저장 중 오류가 발생했습니다."); |
|
161 |
+ if (error.response && error.response.data && error.response.data.message) { |
|
162 |
+ alert(`오류: ${error.response.data.message}`); |
|
163 |
+ } |
|
164 |
+ } |
|
158 | 165 |
}, |
159 |
- getCurrentTime() { |
|
160 |
- const now = new Date(); |
|
161 |
- const hours = String(now.getHours()).padStart(2, '0'); |
|
162 |
- const minutes = String(now.getMinutes()).padStart(2, '0'); |
|
163 |
- const seconds = String(now.getSeconds()).padStart(2, '0'); |
|
164 |
- return `${hours}:${minutes}:${seconds}`; |
|
165 |
- }, |
|
166 |
- getCategoryClass(category) { |
|
167 |
- switch (category) { |
|
168 |
- case '용역': return 'category-service'; |
|
169 |
- case '내부': return 'category-internal'; |
|
170 |
- case '국가과제': return 'category-government'; |
|
171 |
- default: return ''; |
|
166 |
+ async submitRepairForm() { |
|
167 |
+ const confirmDelete = confirm("정말로 사용자를 복구시키겠습니까?\n이 작업은 되돌릴 수 없습니다."); |
|
168 |
+ |
|
169 |
+ if (!confirmDelete) { // 사용자가 '아니오'를 눌렀을 때 |
|
170 |
+ alert("사용자 복구구가 취소되었습니다."); |
|
171 |
+ return; // 함수 실행 중단 |
|
172 |
+ } |
|
173 |
+ |
|
174 |
+ // 2. FormData 객체 생성 |
|
175 |
+ const formData = new FormData(); |
|
176 |
+ |
|
177 |
+ // 수정용 DTO 생성 (기존 데이터 + 수정 데이터) |
|
178 |
+ const updateDTO = { |
|
179 |
+ userNm: this.selectedUser.userNm, |
|
180 |
+ useAt: "Y", |
|
181 |
+ fileId: this.selectedUser.fileId, |
|
182 |
+ }; |
|
183 |
+ |
|
184 |
+ formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], { |
|
185 |
+ type: 'application/json' |
|
186 |
+ })); |
|
187 |
+ |
|
188 |
+ // 4. API 통신 |
|
189 |
+ try { |
|
190 |
+ const response = await updateUsersProc(this.$route.query.id, formData); |
|
191 |
+ |
|
192 |
+ if (response.status === 200) { |
|
193 |
+ alert('사용자 탈퇴퇴가 성공적으로 저장되었습니다!'); |
|
194 |
+ } |
|
195 |
+ } catch (error) { |
|
196 |
+ console.error("사용자 정보 저장 중 오류 발생:", error); |
|
197 |
+ alert("사용자 정보 저장 중 오류가 발생했습니다."); |
|
198 |
+ if (error.response && error.response.data && error.response.data.message) { |
|
199 |
+ alert(`오류: ${error.response.data.message}`); |
|
200 |
+ } |
|
172 | 201 |
} |
173 | 202 |
}, |
174 | 203 |
}, |
... | ... | @@ -179,10 +208,7 @@ |
179 | 208 |
|
180 | 209 |
}, |
181 | 210 |
mounted() { |
182 |
- console.log('main mounted'); |
|
183 |
- setInterval(() => { |
|
184 |
- this.time = this.getCurrentTime(); |
|
185 |
- }, 1000); |
|
211 |
+ this.searchUser(); |
|
186 | 212 |
} |
187 | 213 |
} |
188 | 214 |
</script> |
--- client/views/pages/Manager/hr/hrInsert.vue
+++ client/views/pages/Manager/hr/hrInsert.vue
... | ... | @@ -2,123 +2,140 @@ |
2 | 2 |
<div class="card "> |
3 | 3 |
<div class="card-body "> |
4 | 4 |
<h2 class="card-title">직원 검색</h2> |
5 |
- |
|
6 |
- |
|
7 |
- <div class="name-box flex simple"> |
|
8 |
- <div class="img-area column"> |
|
5 |
+ <div class="name-box flex simple"> |
|
6 |
+ <div class="img-area column"> |
|
9 | 7 |
<div class="img"> |
10 |
- <img :src="previewImg || placeholder" alt="미리보기 이미지" /> |
|
8 |
+ <img :src="croppedImg || placeholder" alt="미리보기 이미지" /> |
|
11 | 9 |
<button class="close-btn" @click="removeImage">×</button> |
12 | 10 |
</div> |
13 | 11 |
<div class="info"> |
14 | 12 |
<div class="file"> |
15 | 13 |
<label for="fileUpload" class="file-label"> |
16 | 14 |
<img :src="file" alt=""> |
17 |
- <p>업로드</p> |
|
15 |
+ <button @click="showCropModal = true"> |
|
16 |
+ <p>업로드</p> |
|
17 |
+ </button> |
|
18 | 18 |
</label> |
19 |
- <input id="fileUpload" type="file" @change="handleFileUpload" accept="image/*" /> |
|
20 | 19 |
</div> |
21 | 20 |
</div> |
22 | 21 |
</div> |
23 |
- <form class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }" |
|
24 |
- @submit.prevent="handleRegister" novalidate > |
|
25 |
- <div class="col-12 "> |
|
26 |
- <div class="col-12 border-x"> |
|
27 |
- <label for="youremail" class="form-label "><p>아이디<p class="require"><img :src="require" alt=""></p></p></label> |
|
28 |
- <input v-model="email" type="text" name="username" class="form-control" required |
|
29 |
- > |
|
30 |
- </div> |
|
31 |
- |
|
32 |
- <div class="col-12 border-x"> |
|
33 |
- <label for="yourPassword" class="form-label"><p>권한<p class="require"><img :src="require" alt=""></p></p></label> |
|
34 |
- <select class="form-select" > |
|
35 |
- <option value="선택">선택</option> |
|
36 |
- <option value=""></option> |
|
37 |
- <option value=""></option> |
|
38 |
- </select> |
|
39 |
- </div> |
|
40 |
- </div> |
|
41 |
- <div class="col-12 "> |
|
42 |
- <div class="col-12 border-x"> |
|
43 |
- <label for="youremail" class="form-label "><p>이름<p class="require"><img :src="require" alt=""></p></p></label> |
|
44 |
- <input v-model="email" type="text" name="username" class="form-control" required |
|
45 |
- > |
|
46 |
- </div> |
|
47 |
- |
|
48 |
- <div class="col-12 border-x"> |
|
49 |
- <label for="yourPassword" class="form-label"><p>부서<p class="require"><img :src="require" alt=""></p></p></label> |
|
50 |
- <input v-model="selectedname" type="password" name="password" class="form-control" |
|
51 |
- required placeholder="주식회사 테이큰 소프트"> |
|
52 |
- <input type="button" class="form-control " value="검색" @click="showPopup = true" /> |
|
53 |
- <BuseoPopup v-if="showPopup" @close="showPopup = false" @select="addApproval"/> |
|
54 |
- </div> |
|
55 |
- </div> |
|
56 |
- <div class="col-12 border-x"> |
|
57 |
- <div class="col-12 border-x"> |
|
58 |
- <label for="youremail" class="form-label"><p>직급<p class="require"><img :src="require" alt=""></p></p></label> |
|
59 |
- <select class="form-select" > |
|
60 |
- <option value="선택">선택</option> |
|
61 |
- <option value=""></option> |
|
62 |
- <option value=""></option> |
|
63 |
- </select> |
|
64 |
- </div> |
|
65 |
- |
|
66 |
- <div class="col-12 border-x"> |
|
67 |
- <label for="yourPassword" class="form-label">직책</label> |
|
68 |
- <select class="form-select" > |
|
69 |
- <option value="선택">선택</option> |
|
70 |
- <option value=""></option> |
|
71 |
- <option value=""></option> |
|
72 |
- </select> |
|
73 |
- </div> |
|
74 |
- </div> |
|
75 |
- |
|
76 |
- |
|
77 |
- </form> |
|
78 |
- |
|
79 |
- </div> |
|
80 |
- <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" |
|
81 |
- @submit.prevent="handleRegister" novalidate style="margin-bottom: 20px;"> |
|
82 |
- <div class="col-12"> |
|
83 |
- <label for="yourName" class="form-label">연락처</label> |
|
84 |
- <input v-model="name" type="text" name="name" class="form-control " id="yourName" |
|
85 |
- placeholder="admin"> |
|
86 |
- </div> |
|
87 |
- <div class="col-12"> |
|
88 |
- <label for="yourName" class="form-label">생년월일</label> |
|
89 |
- <input v-model="name" type="date" name="name" class="form-control" id="yourName" |
|
90 |
- > |
|
22 |
+ <ImageCropper :key="cropperKey" field="avatar" v-model="showCropModal" :width="200" :height="200" |
|
23 |
+ img-format="png" lang-type="ko" @crop-success="cropSuccess" :no-square="true" :round-cropping="true" |
|
24 |
+ :max-size="1024" /> |
|
25 |
+ <div class="tbl-wrap row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }"> |
|
26 |
+ <div class="col-12 "> |
|
27 |
+ <div class="col-12 border-x"> |
|
28 |
+ <label for="youremail" class="form-label "> |
|
29 |
+ <p>아이디 |
|
30 |
+ <p class="require"><img :src="require" alt=""></p> |
|
31 |
+ </p> |
|
32 |
+ </label> |
|
33 |
+ <input v-if="isUserIdCheck" v-model="selectedUser.loginId" type="text" name="loginId" class="form-control" |
|
34 |
+ required readonly> |
|
35 |
+ <input v-else v-model="requestDTO.loginId" type="text" name="loginId" class="form-control" required> |
|
91 | 36 |
</div> |
92 | 37 |
<div class="col-12 border-x"> |
93 |
- <label for="yourName" class="form-label"><p>입사일<p class="require"><img :src="require" alt=""></p></p></label> |
|
94 |
- <input v-model="name" type="date" name="name" class="form-control" id="yourName" required |
|
95 |
- > |
|
38 |
+ <label for="yourPassword" class="form-label"> |
|
39 |
+ <p>권한 |
|
40 |
+ <p class="require"><img :src="require" alt=""></p> |
|
41 |
+ </p> |
|
42 |
+ </label> |
|
43 |
+ <select class="form-select" v-model="selectedAuthor"> |
|
44 |
+ <option value="">선택</option> |
|
45 |
+ <option v-for="author in authorCode" :key="author.authorNm" :value="author.authorCode"> |
|
46 |
+ {{ author.authorNm }} |
|
47 |
+ </option> |
|
48 |
+ </select> |
|
96 | 49 |
</div> |
97 |
- |
|
98 |
- |
|
99 |
- |
|
100 |
- </form> |
|
101 |
- <form class="row g-3 needs-validation detail salary" :class="{ 'was-validated': formSubmitted }" |
|
102 |
- @submit.prevent="handleRegister" novalidate> |
|
103 |
- <div class=" col-12 border-x"><label>연봉<button type="button" title="추가" @click="addSalary"> |
|
50 |
+ </div> |
|
51 |
+ <div class="col-12 "> |
|
52 |
+ <div class="col-12 border-x"> |
|
53 |
+ <label for="youremail" class="form-label "> |
|
54 |
+ <p>이름 |
|
55 |
+ <p class="require"><img :src="require" alt=""></p> |
|
56 |
+ </p> |
|
57 |
+ </label> |
|
58 |
+ <input v-model="requestDTO.userNm" type="text" name="userNm" class="form-control" required> |
|
59 |
+ </div> |
|
60 |
+ <div class="col-12 border-x"> |
|
61 |
+ <label for="yourPassword" class="form-label"> |
|
62 |
+ <p>부서 |
|
63 |
+ <p class="require"><img :src="require" alt=""></p> |
|
64 |
+ </p> |
|
65 |
+ </label> |
|
66 |
+ <input v-model="selectedname" type="text" name="text" class="form-control" required> |
|
67 |
+ <input type="button" class="form-control " value="검색" @click="showPopup = true" /> |
|
68 |
+ <BuseoPopup v-if="showPopup" @close="showPopup = false" @select="addApproval" /> |
|
69 |
+ </div> |
|
70 |
+ </div> |
|
71 |
+ <div class="col-12 border-x"> |
|
72 |
+ <div class="col-12 border-x"> |
|
73 |
+ <label for="youremail" class="form-label"> |
|
74 |
+ <p>직급 |
|
75 |
+ <p class="require"><img :src="require" alt=""></p> |
|
76 |
+ </p> |
|
77 |
+ </label> |
|
78 |
+ <select class="form-select" v-model="selectedClsf"> |
|
79 |
+ <option value="">선택</option> |
|
80 |
+ <option v-for="jobRank in clsfCode" :key="jobRank.codeNm" :value="jobRank.code"> |
|
81 |
+ {{ jobRank.codeNm }} |
|
82 |
+ </option> |
|
83 |
+ </select> |
|
84 |
+ </div> |
|
85 |
+ <div class="col-12 border-x"> |
|
86 |
+ <label for="yourPassword" class="form-label">직책</label> |
|
87 |
+ <select class="form-select" v-model="selectedRepofc"> |
|
88 |
+ <option value="">선택</option> |
|
89 |
+ <option v-for="position in rspofcCode" :key="position.codeNm" :value="position.code"> |
|
90 |
+ {{ position.codeNm }} |
|
91 |
+ </option> |
|
92 |
+ </select> |
|
93 |
+ </div> |
|
94 |
+ </div> |
|
95 |
+ </div> |
|
96 |
+ </div> |
|
97 |
+ <div class="tbl-wrap row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" |
|
98 |
+ style="margin-bottom: 20px;"> |
|
99 |
+ <div class="col-12"> |
|
100 |
+ <label for="phone" class="form-label">연락처</label> |
|
101 |
+ <input v-model="requestDTO.mbtlnum" type="text" name="mbtlnum" class="form-control" id="phone" |
|
102 |
+ placeholder="010-1234-5678" @input="filterPhoneNumber"> |
|
103 |
+ </div> |
|
104 |
+ <div class="col-12"> |
|
105 |
+ <label for="birth" class="form-label">생년월일</label> |
|
106 |
+ <input v-model="requestDTO.brthdy" type="date" name="brthdy" class="form-control" id="yourName"> |
|
107 |
+ </div> |
|
108 |
+ <div class="col-12 border-x"> |
|
109 |
+ <label for="yourName" class="form-label"> |
|
110 |
+ <p>입사일 |
|
111 |
+ <p class="require"><img :src="require" alt=""></p> |
|
112 |
+ </p> |
|
113 |
+ </label> |
|
114 |
+ <input v-model="requestDTO.encpn" type="date" name="encpn" class="form-control" id="yourName" required> |
|
115 |
+ </div> |
|
116 |
+ </div> |
|
117 |
+ <div class="tbl-wrap row g-3 needs-validation detail salary" :class="{ 'was-validated': formSubmitted }"> |
|
118 |
+ <div class=" col-12 border-x"><label>연봉<button type="button" title="추가" @click="addSalary"> |
|
104 | 119 |
<PlusCircleFilled /> |
105 | 120 |
</button></label> |
106 |
- <div class="yearsalary approval-container"> |
|
107 |
- <div class="col-12 border-x addapproval" v-for="(salary, index) in salarys" :key="index"> |
|
108 |
- <input type="text" name="name" class="form-control" v-model="salary.salary" style="width: 200px;" |
|
109 |
- placeholder="년도"> |
|
110 |
- <div> |
|
111 |
- <input type="text" name="name" class="form-control" v-model="salary.total" |
|
112 |
- placeholder="금액"> |
|
113 |
- </div> |
|
121 |
+ <div class="yearsalary approval-container"> |
|
122 |
+ <div class="col-12 border-x addapproval" v-for="(salary, index) in salarys" :key="index"> |
|
123 |
+ <input type="text" name="name" class="form-control" v-model="salary.year" style="width: 200px;" |
|
124 |
+ placeholder="년도" maxlength="4" @input="validateYearInput(salary)"> |
|
125 |
+ <div> |
|
126 |
+ <input type="text" name="name" class="form-control" v-model="salary.amount" placeholder="금액"> |
|
114 | 127 |
</div> |
115 |
- |
|
128 |
+ <button type="button" @click="removeSalary(index)" class="delete-button"> |
|
129 |
+ <CloseOutlined /> |
|
130 |
+ </button> |
|
116 | 131 |
</div> |
117 |
- </div> |
|
118 |
- </form> |
|
119 |
- <div class="buttons"> |
|
120 |
- <button type="reset" class="btn sm sm primary">등록</button> |
|
121 |
- <button type="submit" class="btn sm sm tertiary">취소</button> |
|
132 |
+ </div> |
|
133 |
+ </div> |
|
134 |
+ </div> |
|
135 |
+ <div class="buttons"> |
|
136 |
+ <button type="button" class="btn sm primary" @click="submitUpdateForm" v-if="isUserIdCheck">수정</button> |
|
137 |
+ <button type="button" class="btn sm primary" @click="submitForm" v-else>등록</button> |
|
138 |
+ <button type="button" class="btn sm tertiary" @click="goToManagementList">취소</button> |
|
122 | 139 |
</div> |
123 | 140 |
</div> |
124 | 141 |
|
... | ... | @@ -128,18 +145,24 @@ |
128 | 145 |
|
129 | 146 |
<script> |
130 | 147 |
import GoogleCalendar from "../../../component/GoogleCalendar.vue" |
131 |
-import { SearchOutlined, PlusCircleFilled } from '@ant-design/icons-vue'; |
|
148 |
+import { SearchOutlined, PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue'; |
|
132 | 149 |
import BuseoPopup from "../../../component/Popup/BuseoPopup.vue"; |
150 |
+import { findAuthorsProc } from "../../../../resources/api/author"; |
|
151 |
+import ImageCropper from 'vue-image-crop-upload'; //이미지 자르기기 |
|
152 |
+import FileUploadProgress from '@/views/component/FileUploadProgress.vue'; |
|
153 |
+import { joinProc, findUsersDetProc, updateUsersProc } from "../../../../resources/api/user"; |
|
154 |
+import { findCodesProc } from "../../../../resources/api/code"; |
|
155 |
+ |
|
133 | 156 |
export default { |
134 | 157 |
data() { |
135 | 158 |
return { |
136 | 159 |
showPopup: false, |
137 | 160 |
selectedname: '', |
138 | 161 |
approvals: [], |
139 |
- salarys: [ { |
|
140 |
- salary: '', |
|
141 |
- total: '', |
|
142 |
- },] , |
|
162 |
+ salarys: [{ |
|
163 |
+ year: '', |
|
164 |
+ amount: '', |
|
165 |
+ },], |
|
143 | 166 |
previewImg: null, |
144 | 167 |
placeholder: "/client/resources/img/img1.png", |
145 | 168 |
require: "/client/resources/img/require.png", |
... | ... | @@ -151,112 +174,418 @@ |
151 | 174 |
startbtn: "/client/resources/img/start.png", |
152 | 175 |
stopbtn: "/client/resources/img/stop.png", |
153 | 176 |
moreicon: "/client/resources/img/more.png", |
154 |
- today: new Date().toLocaleDateString('ko-KR', { |
|
155 |
- year: 'numeric', |
|
156 |
- month: '2-digit', |
|
157 |
- day: '2-digit', |
|
158 |
- weekday: 'short', |
|
159 |
- }), |
|
160 |
- time: this.getCurrentTime(), |
|
161 | 177 |
listData: Array.from({ length: 20 }, (_, i) => ({ |
162 |
- department: `부서 ${i + 1}`, |
|
163 |
- name: `이름 ${i + 1}`, |
|
164 |
- position: `직급 ${i + 1}` |
|
165 |
- })) |
|
178 |
+ department: `부서 ${i + 1}`, |
|
179 |
+ name: `이름 ${i + 1}`, |
|
180 |
+ position: `직급 ${i + 1}` |
|
181 |
+ })), |
|
182 |
+ |
|
183 |
+ // 코드 조회 |
|
184 |
+ serachRequest: { |
|
185 |
+ searchType: "upperCd", |
|
186 |
+ searchText: null, |
|
187 |
+ }, |
|
188 |
+ // 직급코드 |
|
189 |
+ clsfCode: [], |
|
190 |
+ // 직책코드 |
|
191 |
+ rspofcCode: [], |
|
192 |
+ // 권한 코드 |
|
193 |
+ authorCode: [], |
|
194 |
+ |
|
195 |
+ showCropModal: false, // ImageCropper 모달을 보여줄지 말지 결정하는 변수 |
|
196 |
+ croppedImg: null, // 최종 크롭되어 표시될 이미지의 Data URL 또는 서버 이미지 URL |
|
197 |
+ |
|
198 |
+ cropperKey: 0, |
|
199 |
+ requestDTO: { |
|
200 |
+ loginId: null, // @NotBlank, @Size, @Pattern |
|
201 |
+ clsf: null, // 선택된 직급의 코드 (예: "RESP_JRM") |
|
202 |
+ rspofc: null, // 선택된 직책의 코드 (예: "RSP_DR") |
|
203 |
+ userNm: null, // @NotBlank, @Size |
|
204 |
+ password: null, // @NotBlank, @Size, @Pattern (이 화면에서 password 받는다면) |
|
205 |
+ mbtlnum: null, // 휴대폰 |
|
206 |
+ telno: null, // 전화번호 (현재 화면에 없으면 null 유지) |
|
207 |
+ email: null, // 이메일 (현재 화면에 없으면 null 유지) |
|
208 |
+ zip: null, // 우편번호 (현재 화면에 없으면 null 유지) |
|
209 |
+ adres: null, // 주소 (현재 화면에 없으면 null 유지) |
|
210 |
+ detailAdres: null, // 상세주소 (현재 화면에 없으면 null 유지) |
|
211 |
+ fileId: null, // 파일 아이디 (파일 저장 후 백엔드에서 반환될 값, 직접 입력X) |
|
212 |
+ brthdy: null, // 생년월일 (YYYY-MM-DD 문자열) |
|
213 |
+ encpn: null, // 입사일 (YYYY-MM-DD 문자열) |
|
214 |
+ authorList: [], // 선택된 권한의 ID들을 담을 배열 (UserAuthorVO 리스트 형태로 변환하여 전송) |
|
215 |
+ }, |
|
216 |
+ selectedAuthor: '', |
|
217 |
+ selectedClsf: '', |
|
218 |
+ selectedRepofc: '', |
|
219 |
+ |
|
220 |
+ |
|
221 |
+ |
|
222 |
+ copyAuthor: null, |
|
223 |
+ selectedUser: {}, |
|
224 |
+ isUserIdCheck: false, |
|
166 | 225 |
} |
167 | 226 |
}, |
168 | 227 |
components: { |
169 |
- SearchOutlined, PlusCircleFilled, BuseoPopup |
|
228 |
+ SearchOutlined, PlusCircleFilled, BuseoPopup, CloseOutlined, findAuthorsProc, ImageCropper, FileUploadProgress, joinProc, findCodesProc |
|
229 |
+ }, |
|
230 |
+ created() { |
|
231 |
+ if (this.$route.query.id != null) { |
|
232 |
+ this.searchUser(); |
|
233 |
+ }; |
|
234 |
+ this.clsfTypeCodes(); // 직급 종류 조회 |
|
235 |
+ this.rspofcTypeCodes(); // 직책 종류 조회 |
|
236 |
+ this.authorTypeCodes(); // 권한 종류 조회 |
|
170 | 237 |
}, |
171 | 238 |
methods: { |
172 |
- addApproval(selectedUser) { |
|
239 |
+ //연도 4자리 숫자만 기록록 |
|
240 |
+ validateYearInput(salaryItem) { |
|
241 |
+ // 1. 숫자 이외의 문자 제거 (정규식 사용) |
|
242 |
+ salaryItem.year = salaryItem.year.replace(/[^0-9]/g, ''); |
|
243 |
+ |
|
244 |
+ // 2. 길이가 4자를 초과하면 잘라내기 |
|
245 |
+ if (salaryItem.year.length > 4) { |
|
246 |
+ salaryItem.year = salaryItem.year.slice(0, 4); |
|
247 |
+ } |
|
248 |
+ // console.log("Current year value:", salaryItem.year); // 디버깅용 |
|
249 |
+ }, |
|
250 |
+ dataURLtoFile(dataurl, filename) { |
|
251 |
+ var arr = dataurl.split(','), |
|
252 |
+ mime = arr[0].match(/:(.*?);/)[1], |
|
253 |
+ bstr = atob(arr[1]), |
|
254 |
+ n = bstr.length, |
|
255 |
+ u8arr = new Uint8Array(n); |
|
256 |
+ while (n--) { |
|
257 |
+ u8arr[n] = bstr.charCodeAt(n); |
|
258 |
+ } |
|
259 |
+ return new File([u8arr], filename, { type: mime }); |
|
260 |
+ }, |
|
261 |
+ |
|
262 |
+ // 연락처 숫자만 기록 |
|
263 |
+ filterPhoneNumber() { |
|
264 |
+ if (this.requestDTO.mbtlnum) { |
|
265 |
+ this.requestDTO.mbtlnum = this.requestDTO.mbtlnum.replace(/\D/g, ''); |
|
266 |
+ } |
|
267 |
+ }, |
|
268 |
+ // 취소 누를 시 직원관리 페이지로 이동 |
|
269 |
+ goToManagementList() { |
|
270 |
+ this.$router.push('/hr-management/hrManagement.page'); |
|
271 |
+ }, |
|
272 |
+ // 공통코드 직급 |
|
273 |
+ async clsfTypeCodes() { |
|
274 |
+ this.clsfCode = []; // 초기화 |
|
275 |
+ this.serachRequest.searchType = "UPPER_CODE" |
|
276 |
+ this.serachRequest.searchText = "clsf_code"; // 공통코드 분류코드 (직급) |
|
277 |
+ |
|
278 |
+ try { |
|
279 |
+ const response = await findCodesProc(this.serachRequest); |
|
280 |
+ if (response.status === 200) { |
|
281 |
+ this.clsfCode = response.data.data.codes; // API 응답에서 카테고리 목록을 가져옴 |
|
282 |
+ } |
|
283 |
+ } catch (error) { |
|
284 |
+ console.error("검색 중 오류 발생:", error); |
|
285 |
+ } |
|
286 |
+ }, |
|
287 |
+ // 공통코드 직책 |
|
288 |
+ async rspofcTypeCodes() { |
|
289 |
+ this.clsfCode = []; // 초기화 |
|
290 |
+ this.serachRequest.searchType = "UPPER_CODE" |
|
291 |
+ this.serachRequest.searchText = "rspofc_code"; // 공통코드 분류코드 (직급) |
|
292 |
+ |
|
293 |
+ try { |
|
294 |
+ const response = await findCodesProc(this.serachRequest); |
|
295 |
+ if (response.status === 200) { |
|
296 |
+ this.rspofcCode = response.data.data.codes; // API 응답에서 카테고리 목록을 가져옴 |
|
297 |
+ } |
|
298 |
+ } catch (error) { |
|
299 |
+ console.error("검색 중 오류 발생:", error); |
|
300 |
+ } |
|
301 |
+ }, |
|
302 |
+ // 공통코드 권한 |
|
303 |
+ async authorTypeCodes() { |
|
304 |
+ this.authorCode = []; // 초기화 |
|
305 |
+ try { |
|
306 |
+ const response = await findAuthorsProc(); |
|
307 |
+ if (response.status === 200) { |
|
308 |
+ this.authorCode = response.data.data.authors; // API 응답에서 카테고리 목록을 가져옴 |
|
309 |
+ } |
|
310 |
+ } catch (error) { |
|
311 |
+ console.error("검색 중 오류 발생:", error); |
|
312 |
+ } |
|
313 |
+ }, |
|
314 |
+ addApproval(user) { |
|
173 | 315 |
this.approvals.push({ |
174 |
- name: selectedUser.name |
|
316 |
+ name: user.name |
|
175 | 317 |
}); |
176 | 318 |
|
177 |
- this.selectedname = selectedUser.name; // 입력창에 표시 |
|
319 |
+ this.selectedname = user.department; // 입력창에 표시 |
|
178 | 320 |
this.showPopup = false; |
179 | 321 |
}, |
180 | 322 |
addSalary() { |
181 | 323 |
this.salarys.push({ |
182 |
- salary: '', |
|
183 |
- total: '', |
|
324 |
+ year: '', |
|
325 |
+ amount: '', |
|
184 | 326 |
}); |
185 | 327 |
}, |
186 |
- handleFileUpload(event) { |
|
187 |
- const file = event.target.files[0]; |
|
188 |
- if (file && file.type.startsWith('image/')) { |
|
189 |
- const reader = new FileReader(); |
|
190 |
- reader.onload = (e) => { |
|
191 |
- this.previewImg = e.target.result; // 파일 읽기 완료 후 미리보기 이미지 설정 |
|
192 |
- }; |
|
193 |
- reader.readAsDataURL(file); // 파일을 데이터 URL로 읽기 |
|
194 |
- } else { |
|
195 |
- alert('이미지 파일만 선택할 수 있습니다.'); |
|
328 |
+ removeSalary(index) { |
|
329 |
+ this.salarys.splice(index, 1); |
|
330 |
+ }, |
|
331 |
+ removeImage() { |
|
332 |
+ this.croppedImg = null; |
|
333 |
+ this.cropperKey++; |
|
334 |
+ }, |
|
335 |
+ cropSuccess(imgDataUrl) { |
|
336 |
+ this.croppedImg = imgDataUrl; |
|
337 |
+ this.showCropModal = false; |
|
338 |
+ }, |
|
339 |
+ //등록용 |
|
340 |
+ async submitForm() { |
|
341 |
+ // 1. 프론트엔드 유효성 검사 (네 style인 alert + return 방식) |
|
342 |
+ if (this.$isEmpty(this.requestDTO.loginId)) { alert("아이디를 입력해 주세요."); return; } |
|
343 |
+ if (this.$isEmpty(this.requestDTO.userNm)) { alert("이름을 입력해 주세요."); return; } |
|
344 |
+ if (this.$isEmpty(this.requestDTO.clsf)) { alert("직급을 선택해 주세요."); return; } |
|
345 |
+ if (this.$isEmpty(this.requestDTO.encpn)) { alert("입사일을 선택해 주세요."); return; } |
|
346 |
+ if (this.$isEmpty(this.requestDTO.authorList) || this.requestDTO.authorList.length === 0) { |
|
347 |
+ alert("권한을 선택해 주세요."); return; |
|
348 |
+ } |
|
349 |
+ |
|
350 |
+ // 2. FormData 객체 생성 및 데이터 추가 |
|
351 |
+ const formData = new FormData(); |
|
352 |
+ |
|
353 |
+ formData.append('joinDTO', new Blob([JSON.stringify(this.requestDTO)], { |
|
354 |
+ type: 'application/json' |
|
355 |
+ })); |
|
356 |
+ |
|
357 |
+ formData.append('salarys', new Blob([JSON.stringify(this.salarys)], { |
|
358 |
+ type: 'application/json' |
|
359 |
+ })); |
|
360 |
+ |
|
361 |
+ // 파일이 있다면 File 객체로 변환하여 'multipartFiles' 필드명으로 추가 |
|
362 |
+ if (this.croppedImg) { |
|
363 |
+ const profileImageFile = this.dataURLtoFile(this.croppedImg, 'profile_image.png'); |
|
364 |
+ formData.append("multipartFiles", profileImageFile); |
|
365 |
+ } |
|
366 |
+ |
|
367 |
+ // 3. API 통신 (fileClient 사용) |
|
368 |
+ try { |
|
369 |
+ const response = await joinProc(formData); |
|
370 |
+ |
|
371 |
+ if (response.status === 200) { |
|
372 |
+ alert('사용자 정보가 성공적으로 저장되었습니다!'); |
|
373 |
+ // 성공 시 페이지 이동 (예: this.$router.push('/hr-management/hrManagement.page')) |
|
374 |
+ } |
|
375 |
+ } catch (error) { |
|
376 |
+ console.error("사용자 정보 저장 중 오류 발생:", error); |
|
377 |
+ alert("사용자 정보 저장 중 오류가 발생했습니다."); |
|
378 |
+ if (error.response && error.response.data && error.response.data.message) { |
|
379 |
+ alert(`오류: ${error.response.data.message}`); |
|
380 |
+ } |
|
196 | 381 |
} |
197 | 382 |
}, |
383 |
+ //수정용 |
|
384 |
+ async submitUpdateForm() { |
|
385 |
+ // 1. 프론트엔드 유효성 검사 |
|
386 |
+ if (this.$isEmpty(this.requestDTO.userNm)) { alert("이름을 입력해 주세요."); return; } |
|
387 |
+ if (this.$isEmpty(this.requestDTO.clsf)) { alert("직급을 선택해 주세요."); return; } |
|
388 |
+ if (this.$isEmpty(this.requestDTO.encpn)) { alert("입사일을 선택해 주세요."); return; } |
|
389 |
+ if (this.$isEmpty(this.requestDTO.authorList) || this.requestDTO.authorList.length === 0) { |
|
390 |
+ alert("권한을 선택해 주세요."); return; |
|
391 |
+ } |
|
198 | 392 |
|
199 |
- // 이미지 삭제 함수 |
|
200 |
- removeImage() { |
|
201 |
- this.previewImg = null; // 미리보기 이미지 삭제 |
|
202 |
- this.$refs.fileUpload.value = null; // 파일 input 초기화 |
|
203 |
- }, |
|
204 |
- formatBudget(amount) { |
|
205 |
- return new Intl.NumberFormat().format(amount) + ' 원'; |
|
206 |
- }, |
|
207 |
- isPastPeriod(period) { |
|
208 |
- // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출 |
|
209 |
- const endDateStr = period.split('~')[1]?.trim(); |
|
210 |
- if (!endDateStr) return false; |
|
393 |
+ // 2. FormData 객체 생성 |
|
394 |
+ const formData = new FormData(); |
|
395 |
+ let updateDTO = {}; |
|
211 | 396 |
|
212 |
- const endDate = new Date(endDateStr); |
|
213 |
- const today = new Date(); |
|
397 |
+ if (this.selectedAuthor == this.copyAuthor) { |
|
398 |
+ // 수정용 DTO 생성 (기존 데이터 + 수정 데이터) |
|
399 |
+ updateDTO = { |
|
400 |
+ ...this.requestDTO, |
|
401 |
+ salaryList: this.salarys, |
|
402 |
+ authorUpdateCheck: false, |
|
403 |
+ }; |
|
404 |
+ } else { |
|
405 |
+ updateDTO = { |
|
406 |
+ ...this.requestDTO, |
|
407 |
+ salaryList: this.salarys, |
|
408 |
+ authorUpdateCheck: true, |
|
409 |
+ }; |
|
410 |
+ } |
|
214 | 411 |
|
215 |
- // 현재 날짜보다 과거면 true |
|
216 |
- return endDate < today; |
|
217 |
- }, |
|
218 |
- getStatusClass(status) { |
|
219 |
- return status === 'active' ? 'status-active' : 'status-inactive'; |
|
220 |
- }, |
|
221 |
- getStatusClass(status) { |
|
222 |
- if (status === '미진행') return 'status-pending'; |
|
223 |
- if (status === '진행중') return 'status-approved'; |
|
412 |
+ formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], { |
|
413 |
+ type: 'application/json' |
|
414 |
+ })); |
|
224 | 415 |
|
225 |
- // Default empty string |
|
226 |
- return ''; |
|
416 |
+ // 3. 파일 처리 |
|
417 |
+ // 기존 파일이 그대로 유지되는 경우 (fileId만 전송) |
|
418 |
+ if (this.selectedUser.fileId && !this.croppedImg.startsWith('data:')) { |
|
419 |
+ // 기존 파일 ID 유지 |
|
420 |
+ updateDTO.fileId = this.selectedUser.fileId; |
|
421 |
+ } |
|
422 |
+ // 새로운 파일이 업로드된 경우 |
|
423 |
+ else if (this.croppedImg && this.croppedImg.startsWith('data:')) { |
|
424 |
+ const profileImageFile = this.dataURLtoFile(this.croppedImg, 'profile_image.png'); |
|
425 |
+ formData.append("multipartFiles", profileImageFile); |
|
426 |
+ // 기존 파일 ID 제거 (새 파일로 교체) |
|
427 |
+ updateDTO.fileId = null; |
|
428 |
+ } |
|
429 |
+ |
|
430 |
+ // 4. API 통신 |
|
431 |
+ try { |
|
432 |
+ const response = await updateUsersProc(this.$route.query.id, formData); |
|
433 |
+ |
|
434 |
+ if (response.status === 200) { |
|
435 |
+ alert('사용자 정보가 성공적으로 저장되었습니다!'); |
|
436 |
+ // 성공 시 페이지 이동 (예: this.$router.push('/hr-management/hrManagement.page')) |
|
437 |
+ } |
|
438 |
+ } catch (error) { |
|
439 |
+ console.error("사용자 정보 저장 중 오류 발생:", error); |
|
440 |
+ alert("사용자 정보 저장 중 오류가 발생했습니다."); |
|
441 |
+ if (error.response && error.response.data && error.response.data.message) { |
|
442 |
+ alert(`오류: ${error.response.data.message}`); |
|
443 |
+ } |
|
444 |
+ } |
|
227 | 445 |
}, |
228 |
- getCurrentTime() { |
|
229 |
- const now = new Date(); |
|
230 |
- const hours = String(now.getHours()).padStart(2, '0'); |
|
231 |
- const minutes = String(now.getMinutes()).padStart(2, '0'); |
|
232 |
- const seconds = String(now.getSeconds()).padStart(2, '0'); |
|
233 |
- return `${hours}:${minutes}:${seconds}`; |
|
234 |
- }, |
|
235 |
- getCategoryClass(category) { |
|
236 |
- switch (category) { |
|
237 |
- case '용역': return 'category-service'; |
|
238 |
- case '내부': return 'category-internal'; |
|
239 |
- case '국가과제': return 'category-government'; |
|
240 |
- default: return ''; |
|
446 |
+ //사용자 정보 상세 조회 |
|
447 |
+ async searchUser() { |
|
448 |
+ try { |
|
449 |
+ const response = await findUsersDetProc(this.$route.query.id); |
|
450 |
+ if (response.status === 200) { |
|
451 |
+ this.selectedUser = response.data.data.user; // API 응답에서 카테고리 목록을 가져옴 |
|
452 |
+ |
|
453 |
+ this.requestDTO = { |
|
454 |
+ userNm: this.selectedUser.userNm, |
|
455 |
+ clsf: this.selectedUser.clsf, |
|
456 |
+ rspofc: this.selectedUser.rspofc || '', |
|
457 |
+ mbtlnum: this.selectedUser.mbtlnum, |
|
458 |
+ telno: this.selectedUser.telno, |
|
459 |
+ email: this.selectedUser.email, |
|
460 |
+ zip: this.selectedUser.zip, |
|
461 |
+ adres: this.selectedUser.adres, |
|
462 |
+ detailAdres: this.selectedUser.detailAdres, |
|
463 |
+ brthdy: this.selectedUser.brthdy, |
|
464 |
+ encpn: this.selectedUser.encpn, |
|
465 |
+ userSttus: this.selectedUser.userSttus, |
|
466 |
+ useAt: this.selectedUser.useAt, |
|
467 |
+ fileId: this.selectedUser.fileId, // 기존 파일 ID 보존 |
|
468 |
+ authorList: this.selectedUser.authorList |
|
469 |
+ } |
|
470 |
+ this.selectedAuthor = this.selectedUser.authorList[0]?.authorCode; |
|
471 |
+ this.selectedClsf = this.selectedUser.clsf; |
|
472 |
+ this.selectedRepofc = this.selectedUser.rspofc || ''; |
|
473 |
+ |
|
474 |
+ this.croppedImg = this.selectedUser.thumbnail?.filePath || null; |
|
475 |
+ |
|
476 |
+ // 연봉 정보 설정 |
|
477 |
+ this.salarys = this.selectedUser.salaryList || []; |
|
478 |
+ |
|
479 |
+ this.copyAuthor = this.selectedUser.authorList[0]?.authorCode; |
|
480 |
+ this.isUserIdCheck = true; |
|
481 |
+ console.log("유저 정보 로드 완료", this.selectedUser); |
|
482 |
+ } |
|
483 |
+ } catch (error) { |
|
484 |
+ console.error("검색 중 오류 발생:", error); |
|
241 | 485 |
} |
242 | 486 |
}, |
243 | 487 |
}, |
244 | 488 |
watch: { |
245 |
- |
|
489 |
+ // 직급 선택 감지 -> requestDTO.clsf 업데이트 |
|
490 |
+ selectedClsf(newVal) { |
|
491 |
+ this.requestDTO.clsf = newVal ? newVal : null; // 선택된 직급 객체의 'code'를 requestDTO.clsf에 할당 |
|
492 |
+ }, |
|
493 |
+ // 직책 선택 감지 -> requestDTO.rspofc 업데이트 |
|
494 |
+ selectedRepofc(newVal) { |
|
495 |
+ this.requestDTO.rspofc = newVal ? newVal : null; // 선택된 직책 객체의 'code'를 requestDTO.rspofc에 할당 |
|
496 |
+ }, |
|
497 |
+ // 권한 선택 감지 -> requestDTO.authorList 업데이트 |
|
498 |
+ selectedAuthor(newVal) { |
|
499 |
+ if (newVal) { |
|
500 |
+ // UserAuthorVO 형태에 맞춰 authorId와 authorNm을 포함하는 객체를 생성하여 배열에 담음 |
|
501 |
+ // 백엔드 UserAuthorVO에 정확히 authorId, authorNm 필드가 있는지 확인 필요 |
|
502 |
+ this.requestDTO.authorList = [{ |
|
503 |
+ authorCode: newVal, |
|
504 |
+ }]; |
|
505 |
+ } else { |
|
506 |
+ this.requestDTO.authorList = []; |
|
507 |
+ } |
|
508 |
+ }, |
|
246 | 509 |
}, |
247 | 510 |
computed: { |
248 |
- |
|
249 | 511 |
}, |
250 | 512 |
mounted() { |
251 |
- console.log('main mounted'); |
|
252 |
- setInterval(() => { |
|
253 |
- this.time = this.getCurrentTime(); |
|
254 |
- }, 1000); |
|
255 |
- } |
|
513 |
+ }, |
|
256 | 514 |
} |
257 | 515 |
</script> |
258 | 516 |
<style scoped> |
259 | 517 |
tr { |
260 | 518 |
cursor: pointer; |
261 | 519 |
} |
520 |
+ |
|
521 |
+.profile-container { |
|
522 |
+ padding: 20px; |
|
523 |
+ text-align: center; |
|
524 |
+} |
|
525 |
+ |
|
526 |
+.info { |
|
527 |
+ margin-bottom: 20px; |
|
528 |
+} |
|
529 |
+ |
|
530 |
+.profile-image-area { |
|
531 |
+ width: 150px; |
|
532 |
+ height: 150px; |
|
533 |
+ border-radius: 50%; |
|
534 |
+ /* 원형으로 만들게 */ |
|
535 |
+ overflow: hidden; |
|
536 |
+ /* 영역 밖으로 넘치는 이미지 자르기 */ |
|
537 |
+ margin: 0 auto 20px; |
|
538 |
+ /* 가운데 정렬 및 아래 여백 */ |
|
539 |
+ border: 2px solid #eee; |
|
540 |
+ /* 테두리 */ |
|
541 |
+ display: flex; |
|
542 |
+ align-items: center; |
|
543 |
+ justify-content: center; |
|
544 |
+ background-color: #f9f9f9; |
|
545 |
+ /* 기본 배경색 */ |
|
546 |
+} |
|
547 |
+ |
|
548 |
+.cropped-profile-img { |
|
549 |
+ width: 100%; |
|
550 |
+ height: 100%; |
|
551 |
+ object-fit: cover; |
|
552 |
+ /* 이미지를 영역에 꽉 채우되 비율 유지 */ |
|
553 |
+} |
|
554 |
+ |
|
555 |
+.default-profile-icon { |
|
556 |
+ font-size: 60px; |
|
557 |
+ color: #ccc; |
|
558 |
+ display: flex; |
|
559 |
+ align-items: center; |
|
560 |
+ justify-content: center; |
|
561 |
+ width: 100%; |
|
562 |
+ height: 100%; |
|
563 |
+} |
|
564 |
+ |
|
565 |
+.remove-btn { |
|
566 |
+ display: inline-block; |
|
567 |
+ /* 버튼 형태로 보이기 위해 */ |
|
568 |
+ padding: 10px 20px; |
|
569 |
+ margin: 5px; |
|
570 |
+ background-color: #007bff; |
|
571 |
+ color: white; |
|
572 |
+ border: none; |
|
573 |
+ border-radius: 5px; |
|
574 |
+ cursor: pointer; |
|
575 |
+ font-size: 16px; |
|
576 |
+ transition: background-color 0.3s ease; |
|
577 |
+} |
|
578 |
+ |
|
579 |
+.file-label.upload-btn:hover, |
|
580 |
+.remove-btn:hover { |
|
581 |
+ background-color: #0056b3; |
|
582 |
+} |
|
583 |
+ |
|
584 |
+.remove-btn { |
|
585 |
+ background-color: #dc3545; |
|
586 |
+} |
|
587 |
+ |
|
588 |
+.remove-btn:hover { |
|
589 |
+ background-color: #c82333; |
|
590 |
+} |
|
262 | 591 |
</style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/Manager/hr/hrManagement.vue
+++ client/views/pages/Manager/hr/hrManagement.vue
... | ... | @@ -8,15 +8,24 @@ |
8 | 8 |
<select name="" id="" class="form-select"> |
9 | 9 |
<option value="">부서</option> |
10 | 10 |
</select> |
11 |
- <select name="" id="" class="form-select"> |
|
11 |
+ <select class="form-select" v-model="selectedClsf"> |
|
12 | 12 |
<option value="">직급</option> |
13 |
+ <option v-for="jobRank in clsfCode" :key="jobRank.codeNm" :value="jobRank"> |
|
14 |
+ {{ jobRank.codeNm }} |
|
15 |
+ </option> |
|
13 | 16 |
</select> |
14 |
- <select name="" id="" class="form-select"> |
|
17 |
+ <select class="form-select" v-model="selectedRepofc"> |
|
15 | 18 |
<option value="">직책</option> |
19 |
+ <option v-for="position in rspofcCode" :key="position.codeNm" :value="position"> |
|
20 |
+ {{ position.codeNm }} |
|
21 |
+ </option> |
|
16 | 22 |
</select> |
17 | 23 |
<div class="sch-input"> |
18 |
- <input type="text" class="form-control" placeholder="직원명"> |
|
19 |
- <button class="ico-sch"><SearchOutlined /></button> |
|
24 |
+ <input type="text" class="form-control" v-model="userSearchReqDTO.searchText" @keyup.enter="searchUsers" |
|
25 |
+ placeholder="직원명"> |
|
26 |
+ <button class="ico-sch" @click="searchUsers"> |
|
27 |
+ <SearchOutlined /> |
|
28 |
+ </button> |
|
20 | 29 |
</div> |
21 | 30 |
</div> |
22 | 31 |
</div> |
... | ... | @@ -38,43 +47,25 @@ |
38 | 47 |
</thead> |
39 | 48 |
<!-- 동적으로 <td> 생성 --> |
40 | 49 |
<tbody> |
41 |
- <tr v-for="(item, index) in listData" :key="index" @click="goToDetailPage(item)"> |
|
42 |
- <td>{{ item.department }}</td> |
|
43 |
- <td>{{ item.position }}</td> |
|
44 |
- <td>{{ item.role }}</td> |
|
45 |
- <td>{{ item.userId }}</td> |
|
46 |
- <td>{{ item.name }}</td> |
|
47 |
- <td>{{ item.joinDate }}</td> |
|
48 |
- <td> |
|
49 |
- <button class="btn sm tertiary xsm" @click.stop="resetPassword(item)">초기화</button> |
|
50 |
- </td> |
|
50 |
+ <tr v-for="(item, index) in selectedUsers" :key="index" @click="goToDetailPage(item)" :class="{ 'inactive-user': item.useAt === 'N' }"> |
|
51 |
+ <td>{{ item.deptNm }}</td> |
|
52 |
+ <td>{{ item.clsfNm }}</td> |
|
53 |
+ <td>{{ item.rspofcNm }}</td> |
|
54 |
+ <td>{{ item.loginId }}</td> |
|
55 |
+ <td>{{ item.userNm }}</td> |
|
56 |
+ <td>{{ item.encpn }}</td> |
|
57 |
+ <td> |
|
58 |
+ <button class="btn tertiary xsm" @click.stop="resetPassword(item)">초기화</button> |
|
59 |
+ </td> |
|
51 | 60 |
</tr> |
52 | 61 |
</tbody> |
53 | 62 |
</table> |
54 | 63 |
|
55 | 64 |
</div> |
56 |
- <div class="pagination"> |
|
57 |
- <ul> |
|
58 |
- <!-- 왼쪽 화살표 (이전 페이지) --> |
|
59 |
- <li class="arrow" :class="{ disabled: currentPage === 1 }" @click="changePage(currentPage - 1)"> |
|
60 |
- < |
|
61 |
- </li> |
|
62 |
- |
|
63 |
- <!-- 페이지 번호 --> |
|
64 |
- <li v-for="page in totalPages" :key="page" :class="{ active: currentPage === page }" |
|
65 |
- @click="changePage(page)"> |
|
66 |
- {{ page }} |
|
67 |
- </li> |
|
68 |
- |
|
69 |
- <!-- 오른쪽 화살표 (다음 페이지) --> |
|
70 |
- <li class="arrow" :class="{ disabled: currentPage === totalPages }" @click="changePage(currentPage + 1)"> |
|
71 |
- > |
|
72 |
- </li> |
|
73 |
- </ul> |
|
74 |
- </div> |
|
65 |
+ <Pagination :search="userSearchReqDTO" @onChange="fnChangeCurrentPage" /> |
|
75 | 66 |
<div class="buttons"> |
76 |
- <button class="btn sm primary" type="submit" @click="goToPage('등록')">등록</button> |
|
77 |
- </div> |
|
67 |
+ <button class="btn primary" type="submit" @click="goToPage('등록')">등록</button> |
|
68 |
+ </div> |
|
78 | 69 |
</div> |
79 | 70 |
</div> |
80 | 71 |
</div> |
... | ... | @@ -83,6 +74,9 @@ |
83 | 74 |
<script> |
84 | 75 |
import { ref } from 'vue'; |
85 | 76 |
import { SearchOutlined } from '@ant-design/icons-vue'; |
77 |
+import { findUsersProc, updatePassword } from "../../../../resources/api/user" |
|
78 |
+import Pagination from '../../../component/Pagination.vue'; |
|
79 |
+import { findCodesProc } from '../../../../resources/api/code'; |
|
86 | 80 |
export default { |
87 | 81 |
data() { |
88 | 82 |
return { |
... | ... | @@ -90,86 +84,143 @@ |
90 | 84 |
currentPage: 1, |
91 | 85 |
totalPages: 3, |
92 | 86 |
photoicon: "/client/resources/img/photo_icon.png", |
93 |
- // 데이터 초기화 |
|
94 |
- years: [2023, 2024, 2025], // 연도 목록 |
|
95 |
- months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록 |
|
96 |
- selectedYear: '', |
|
97 |
- selectedMonth: '', |
|
98 |
- listData: [ |
|
99 |
- { |
|
100 |
- department: '인사팀', |
|
101 |
- position: '대리', |
|
102 |
- role: '인사담당', |
|
103 |
- userId: 'honggildong', |
|
104 |
- name: '홍길동', |
|
105 |
- joinDate: '2022-01-15' |
|
106 |
- }, |
|
107 |
-], |
|
108 |
- filteredData: [], |
|
87 |
+ selectedUsers: [], |
|
88 |
+ // 코드 조회 |
|
89 |
+ serachRequest: { |
|
90 |
+ searchType: "upperCd", |
|
91 |
+ searchText: null, |
|
92 |
+ }, |
|
93 |
+ // 직급코드 |
|
94 |
+ clsfCode: [], |
|
95 |
+ // 직책코드 |
|
96 |
+ rspofcCode: [], |
|
97 |
+ selectedClsf: '', |
|
98 |
+ selectedRepofc: '', |
|
99 |
+ userSearchReqDTO: { |
|
100 |
+ searchType: "all", // 'id' 또는 'nm' 등 선택 |
|
101 |
+ searchText: null, // 검색어 입력 |
|
102 |
+ userSttus: null, // 회원상태 선택 (0,1,2,3) |
|
103 |
+ useAt: null, // 사용여부 선택 (Y, N) |
|
104 |
+ dept: null, // 부서 선택 |
|
105 |
+ clsf: null, // 직급 선택 |
|
106 |
+ rspofc: null, // 직책 선택 |
|
107 |
+ |
|
108 |
+ currentPage: 1, |
|
109 |
+ }, |
|
110 |
+ updatePasswordDTO: { |
|
111 |
+ oldPassword:"Ts0511@@1", |
|
112 |
+ newPassword:"Ts0511@@1", |
|
113 |
+ resetAt: true, |
|
114 |
+ }, |
|
109 | 115 |
}; |
110 | 116 |
}, |
111 |
- components:{ |
|
112 |
- SearchOutlined |
|
117 |
+ components: { |
|
118 |
+ SearchOutlined, Pagination, findCodesProc, updatePassword |
|
113 | 119 |
}, |
114 | 120 |
computed: { |
115 | 121 |
}, |
122 |
+ created() { |
|
123 |
+ this.clsfTypeCodes(); // 직급 종류 조회 |
|
124 |
+ this.rspofcTypeCodes(); // 직책 종류 조회 |
|
125 |
+ }, |
|
116 | 126 |
methods: { |
117 |
- resetPassword(user) { |
|
118 |
- // 예: 비밀번호 초기화 로직 |
|
119 |
- console.log(`${user.name} (${user.userId}) 비밀번호 초기화`); |
|
120 |
- // 실제 초기화 API 호출 또는 처리 추가 |
|
121 |
- }, |
|
122 |
- goToDetailPage(item) { |
|
123 |
- // item.id 또는 다른 식별자를 사용하여 URL을 구성할 수 있습니다. |
|
124 |
- this.$router.push({ name: 'hrDetail', query: { id: item.id } }); |
|
125 |
- }, |
|
126 |
- changePage(page) { |
|
127 |
- if (page < 1 || page > this.totalPages) return; |
|
128 |
- this.currentPage = page; |
|
129 |
- this.$emit('page-changed', page); // 필요 시 부모에 알림 |
|
127 |
+ // 페이지 이동 |
|
128 |
+ fnChangeCurrentPage(currentPage) { |
|
129 |
+ this.userSearchReqDTO.currentPage = Number(currentPage); |
|
130 |
+ console.log(this.userSearchReqDTO); |
|
131 |
+ this.$nextTick(() => { |
|
132 |
+ this.searchUsers(); |
|
133 |
+ }); |
|
130 | 134 |
}, |
131 |
- async onClickSubmit() { |
|
132 |
- // `useMutation` 훅을 사용하여 mutation 함수 가져오기 |
|
133 |
- const { mutate, onDone, onError } = useMutation(mygql); |
|
135 |
+ //사용자 정보 전체 조회 |
|
136 |
+ async searchUsers() { |
|
137 |
+ try { |
|
138 |
+ const response = await findUsersProc(this.userSearchReqDTO); |
|
139 |
+ if (response.status === 200) { |
|
140 |
+ this.selectedUsers = response.data.data.users; // API 응답에서 카테고리 목록을 가져옴 |
|
141 |
+ console.log("user 정보 ",this.selectedUsers); |
|
142 |
+ } |
|
143 |
+ } catch (error) { |
|
144 |
+ console.error("검색 중 오류 발생:", error); |
|
145 |
+ } |
|
146 |
+ }, |
|
147 |
+ // 공통코드 직급 |
|
148 |
+ async clsfTypeCodes() { |
|
149 |
+ this.clsfCode = []; // 초기화 |
|
150 |
+ this.serachRequest.searchType = "UPPER_CODE" |
|
151 |
+ this.serachRequest.searchText = "clsf_code"; // 공통코드 분류코드 (직급) |
|
134 | 152 |
|
135 | 153 |
try { |
136 |
- const result = await mutate(); |
|
137 |
- console.log(result); |
|
154 |
+ const response = await findCodesProc(this.serachRequest); |
|
155 |
+ if (response.status === 200) { |
|
156 |
+ this.clsfCode = response.data.data.codes; // API 응답에서 카테고리 목록을 가져옴 |
|
157 |
+ } |
|
138 | 158 |
} catch (error) { |
139 |
- console.error('Mutation error:', error); |
|
159 |
+ console.error("검색 중 오류 발생:", error); |
|
140 | 160 |
} |
161 |
+ }, |
|
162 |
+ // 공통코드 직책 |
|
163 |
+ async rspofcTypeCodes() { |
|
164 |
+ this.clsfCode = []; // 초기화 |
|
165 |
+ this.serachRequest.searchType = "UPPER_CODE" |
|
166 |
+ this.serachRequest.searchText = "rspofc_code"; // 공통코드 분류코드 (직급) |
|
167 |
+ |
|
168 |
+ try { |
|
169 |
+ const response = await findCodesProc(this.serachRequest); |
|
170 |
+ if (response.status === 200) { |
|
171 |
+ this.rspofcCode = response.data.data.codes; // API 응답에서 카테고리 목록을 가져옴 |
|
172 |
+ } |
|
173 |
+ } catch (error) { |
|
174 |
+ console.error("검색 중 오류 발생:", error); |
|
175 |
+ } |
|
176 |
+ }, |
|
177 |
+ async resetPassword(user) { |
|
178 |
+ try { |
|
179 |
+ const response = await updatePassword(user.userId, this.updatePasswordDTO); |
|
180 |
+ if (response.status === 200) { |
|
181 |
+ console.log("비밀번호 초기화"); |
|
182 |
+ } |
|
183 |
+ } catch (error) { |
|
184 |
+ console.error("검색 중 오류 발생:", error); |
|
185 |
+ } |
|
186 |
+ }, |
|
187 |
+ goToDetailPage(item) { |
|
188 |
+ // item.id 또는 다른 식별자를 사용하여 URL을 구성할 수 있습니다. |
|
189 |
+ this.$router.push({ name: 'hrDetail', query: { id: item.userId } }); |
|
141 | 190 |
}, |
142 | 191 |
goToPage(type) { |
143 | 192 |
if (type === '등록') { |
144 | 193 |
this.$router.push({ name: 'hrInsert' }); |
145 | 194 |
} |
146 | 195 |
}, |
147 |
- getStatusClass(status) { |
|
148 |
- if (status === '승인') return 'status-approved'; |
|
149 |
- if (status === '대기') return 'status-pending'; |
|
150 |
- return ''; |
|
151 |
- }, |
|
152 |
- isPastPeriod(period) { |
|
153 |
- // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출 |
|
154 |
- const endDateStr = period.split('~')[1]?.trim(); |
|
155 |
- if (!endDateStr) return false; |
|
156 |
- |
|
157 |
- const endDate = new Date(endDateStr); |
|
158 |
- const today = new Date(); |
|
159 |
- |
|
160 |
- // 현재 날짜보다 과거면 true |
|
161 |
- return endDate < today; |
|
162 |
- } |
|
163 | 196 |
}, |
164 |
- created() { |
|
197 |
+ watch: { |
|
198 |
+ // 직급 선택 감지 -> requestDTO.clsf 업데이트 |
|
199 |
+ selectedClsf(newVal) { |
|
200 |
+ this.userSearchReqDTO.clsf = newVal ? newVal.code : null; // 선택된 직급 객체의 'code'를 requestDTO.clsf에 할당 |
|
201 |
+ }, |
|
202 |
+ // 직책 선택 감지 -> requestDTO.rspofc 업데이트 |
|
203 |
+ selectedRepofc(newVal) { |
|
204 |
+ this.userSearchReqDTO.rspofc = newVal ? newVal.code : null; // 선택된 직책 객체의 'code'를 requestDTO.rspofc에 할당 |
|
205 |
+ }, |
|
165 | 206 |
}, |
166 | 207 |
mounted() { |
167 |
- |
|
168 |
- |
|
208 |
+ this.searchUsers(); |
|
209 |
+ this.clsfTypeCodes(); |
|
210 |
+ this.rspofcTypeCodes(); |
|
169 | 211 |
}, |
170 | 212 |
}; |
171 | 213 |
</script> |
172 | 214 |
|
173 | 215 |
<style scoped> |
174 |
-tr{cursor: pointer;} |
|
216 |
+tr { |
|
217 |
+ cursor: pointer; |
|
218 |
+} |
|
219 |
+.inactive-user { |
|
220 |
+ color: #999999 !important; /* 회색 텍스트 */ |
|
221 |
+} |
|
222 |
+ |
|
223 |
+.inactive-user td { |
|
224 |
+ color: #999999 !important; /* 모든 td 요소를 회색으로 */ |
|
225 |
+} |
|
175 | 226 |
</style> |
--- client/views/pages/Manager/hr/hrSearch.vue
+++ client/views/pages/Manager/hr/hrSearch.vue
... | ... | @@ -6,8 +6,8 @@ |
6 | 6 |
<div class="sch-form-wrap search"> |
7 | 7 |
<div class="input-group"> |
8 | 8 |
<div class="sch-input"> |
9 |
- <input type="text" class="form-control" placeholder="직원명"> |
|
10 |
- <button class="ico-sch"> |
|
9 |
+ <input type="text" class="form-control" placeholder="직원명" v-model="searchReqDTO.searchText" @keyup.enter="searchUsers"> |
|
10 |
+ <button class="ico-sch" @click="searchUsers"> |
|
11 | 11 |
<SearchOutlined /> |
12 | 12 |
</button> |
13 | 13 |
</div> |
... | ... | @@ -24,93 +24,90 @@ |
24 | 24 |
</thead> |
25 | 25 |
<!-- 동적으로 <td> 생성 --> |
26 | 26 |
<tbody> |
27 |
- <tr v-for="(item, index) in listData" :key="index"> |
|
28 |
- <td></td> |
|
29 |
- <td></td> |
|
30 |
- <td></td> |
|
27 |
+ <tr v-for="(item, index) in selectedUsers" :key="index" @click="selectUser(item)" :class="{ 'inactive-user': item.useAt === 'N' }"> |
|
28 |
+ <td>{{ item.deptNm }}</td> |
|
29 |
+ <td>{{ item.userNm }}</td> |
|
30 |
+ <td> |
|
31 |
+ <template v-if="item.rspofcNm"> |
|
32 |
+ {{ item.clsfNm }}({{ item.rspofcNm }}) |
|
33 |
+ </template> |
|
34 |
+ <template v-else> |
|
35 |
+ {{ item.clsfNm }} |
|
36 |
+ </template> |
|
37 |
+ </td> |
|
31 | 38 |
</tr> |
32 | 39 |
</tbody> |
33 | 40 |
</table> |
34 |
- |
|
35 | 41 |
</div> |
36 | 42 |
</div> |
37 |
- |
|
38 | 43 |
<div> |
39 | 44 |
<div class="name-box flex sb simple"> |
40 | 45 |
<div class="img-area" style="width: 170px;"> |
41 |
- <div class="img"><img :src="photoicon" alt=""> |
|
46 |
+ <div class="img"><img :src="selectedUser.file.filePath" :alt="selectedUser.file.fileNm"> |
|
42 | 47 |
</div> |
43 | 48 |
</div> |
44 |
- <form class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }" |
|
49 |
+ <div class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }" |
|
45 | 50 |
@submit.prevent="handleRegister" novalidate> |
46 | 51 |
<div class="col-12"> |
47 | 52 |
<label for="yourName" class="form-label">아이디</label> |
48 |
- <input v-model="name" type="text" name="name" class="form-control" required readonly |
|
53 |
+ <input v-model="name" type="text" name="name" class="form-control" required readonly |
|
49 | 54 |
placeholder="admin"> |
50 | 55 |
</div> |
51 | 56 |
<div class="col-12 "> |
52 | 57 |
<div class="col-12 border-x"> |
53 |
- <label for="youremail" class="form-label ">이름<p class="require"><img :src="require" alt=""></p></label> |
|
54 |
- <input v-model="email" type="text" name="username" class="form-control" required |
|
58 |
+ <label for="userNm" class="form-label ">이름<p class="require"><img :src="require" alt=""></p> |
|
59 |
+ </label> |
|
60 |
+ <input v-model="selectedUser.userNm" type="text" name="username" class="form-control" required |
|
55 | 61 |
readonly> |
56 | 62 |
</div> |
57 |
- |
|
58 | 63 |
<div class="col-12 border-x"> |
59 |
- <label for="yourPassword" class="form-label">부서</label> |
|
60 |
- <input v-model="password" type="password" name="password" class="form-control" |
|
61 |
- required readonly placeholder="주식회사 테이큰 소프트"> |
|
64 |
+ <label for="deptNm" class="form-label">부서</label> |
|
65 |
+ <input v-model="selectedUser.deptNm" type="text" name="password" class="form-control" required |
|
66 |
+ readonly placeholder="주식회사 테이큰 소프트"> |
|
62 | 67 |
</div> |
63 | 68 |
</div> |
64 | 69 |
<div class="col-12 border-x"> |
65 | 70 |
<div class="col-12 border-x"> |
66 |
- <label for="youremail" class="form-label">직급</label> |
|
67 |
- <input v-model="email" type="text" name="username" class="form-control" required readonly |
|
68 |
- placeholder="과장"> |
|
71 |
+ <label for="clsfNm" class="form-label">직급</label> |
|
72 |
+ <input v-model="selectedUser.clsfNm" type="text" name="username" class="form-control" required |
|
73 |
+ readonly> |
|
69 | 74 |
</div> |
70 |
- |
|
71 | 75 |
<div class="col-12 border-x"> |
72 |
- <label for="yourPassword" class="form-label">직책</label> |
|
73 |
- <input v-model="password" type="password" name="password" class="form-control" |
|
74 |
- required readonly placeholder="팀장"> |
|
76 |
+ <label for="rspofcNm" class="form-label">직책</label> |
|
77 |
+ <input v-model="selectedUser.rspofcNm" type="password" name="password" class="form-control" required |
|
78 |
+ readonly> |
|
75 | 79 |
</div> |
76 | 80 |
</div> |
77 |
- |
|
78 |
- |
|
79 |
- </form> |
|
80 |
- |
|
81 |
+ </div> |
|
81 | 82 |
</div> |
82 |
- <form class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" |
|
83 |
+ <div class="row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }" |
|
83 | 84 |
@submit.prevent="handleRegister" novalidate> |
84 | 85 |
<div class="col-12"> |
85 |
- <label for="yourName" class="form-label">연락처</label> |
|
86 |
- <input v-model="name" type="text" name="name" class="form-control" required readonly |
|
86 |
+ <label for="mbtlnum" class="form-label">연락처</label> |
|
87 |
+ <input v-model="selectedUser.mbtlnum" type="text" name="name" class="form-control" required readonly |
|
87 | 88 |
placeholder="admin"> |
88 | 89 |
</div> |
89 | 90 |
<div class="col-12"> |
90 |
- <label for="yourName" class="form-label">생년월일</label> |
|
91 |
- <input v-model="name" type="text" name="name" class="form-control" required readonly |
|
91 |
+ <label for="brthdy" class="form-label">생년월일</label> |
|
92 |
+ <input v-model="selectedUser.brthdy" type="text" name="name" class="form-control" required readonly |
|
92 | 93 |
placeholder="admin"> |
93 | 94 |
</div> |
94 | 95 |
<div class="col-12 border-x"> |
95 |
- <label for="yourName" class="form-label">입사일</label> |
|
96 |
- <input v-model="name" type="text" name="name" class="form-control" required readonly |
|
96 |
+ <label for="encpn" class="form-label">입사일</label> |
|
97 |
+ <input v-model="selectedUser.encpn" type="text" name="name" class="form-control" required readonly |
|
97 | 98 |
placeholder="admin"> |
98 | 99 |
</div> |
99 |
- |
|
100 |
- |
|
101 |
- |
|
102 |
- </form> |
|
100 |
+ </div> |
|
103 | 101 |
</div> |
104 | 102 |
</div> |
105 | 103 |
</div> |
106 |
- |
|
107 | 104 |
</div> |
108 |
- |
|
109 | 105 |
</template> |
110 | 106 |
|
111 | 107 |
<script> |
112 | 108 |
import GoogleCalendar from "../../../component/GoogleCalendar.vue" |
113 | 109 |
import { SearchOutlined } from '@ant-design/icons-vue'; |
110 |
+import { findUsersProc } from "../../../../resources/api/user" |
|
114 | 111 |
export default { |
115 | 112 |
data() { |
116 | 113 |
return { |
... | ... | @@ -121,63 +118,64 @@ |
121 | 118 |
startbtn: "/client/resources/img/start.png", |
122 | 119 |
stopbtn: "/client/resources/img/stop.png", |
123 | 120 |
moreicon: "/client/resources/img/more.png", |
124 |
- today: new Date().toLocaleDateString('ko-KR', { |
|
125 |
- year: 'numeric', |
|
126 |
- month: '2-digit', |
|
127 |
- day: '2-digit', |
|
128 |
- weekday: 'short', |
|
129 |
- }), |
|
130 |
- time: this.getCurrentTime(), |
|
131 |
- listData: Array.from({ length: 20 }, (_, i) => ({ |
|
132 |
- department: `부서 ${i + 1}`, |
|
133 |
- name: `이름 ${i + 1}`, |
|
134 |
- position: `직급 ${i + 1}` |
|
135 |
- })) |
|
121 |
+ selectedUsers: [], |
|
122 |
+ selectedUser: { |
|
123 |
+ file: { |
|
124 |
+ fileNm: "기본이미지", |
|
125 |
+ filePath: "/client/resources/img/photo_icon.png", |
|
126 |
+ }, |
|
127 |
+ loginId: null, |
|
128 |
+ userNm: null, |
|
129 |
+ deptNm: null, |
|
130 |
+ clsfNm: null, |
|
131 |
+ rspofcNm: null, |
|
132 |
+ mbtlnum: null, |
|
133 |
+ brthdy: null, |
|
134 |
+ encpn: null, |
|
135 |
+ }, |
|
136 |
+ searchReqDTO: { |
|
137 |
+ searchType: "nm", |
|
138 |
+ searchText: null, |
|
139 |
+ }, |
|
136 | 140 |
} |
137 | 141 |
}, |
138 | 142 |
components: { |
139 |
- SearchOutlined |
|
143 |
+ SearchOutlined, findUsersProc |
|
140 | 144 |
}, |
141 | 145 |
methods: { |
142 |
- formatBudget(amount) { |
|
143 |
- return new Intl.NumberFormat().format(amount) + ' 원'; |
|
144 |
- }, |
|
145 |
- isPastPeriod(period) { |
|
146 |
- // 예: '2025-05-01 ~ 2025-05-03' → 종료일 추출 |
|
147 |
- const endDateStr = period.split('~')[1]?.trim(); |
|
148 |
- if (!endDateStr) return false; |
|
149 |
- |
|
150 |
- const endDate = new Date(endDateStr); |
|
151 |
- const today = new Date(); |
|
152 |
- |
|
153 |
- // 현재 날짜보다 과거면 true |
|
154 |
- return endDate < today; |
|
155 |
- }, |
|
156 |
- getStatusClass(status) { |
|
157 |
- return status === 'active' ? 'status-active' : 'status-inactive'; |
|
158 |
- }, |
|
159 |
- getStatusClass(status) { |
|
160 |
- if (status === '미진행') return 'status-pending'; |
|
161 |
- if (status === '진행중') return 'status-approved'; |
|
162 |
- |
|
163 |
- // Default empty string |
|
164 |
- return ''; |
|
165 |
- }, |
|
166 |
- getCurrentTime() { |
|
167 |
- const now = new Date(); |
|
168 |
- const hours = String(now.getHours()).padStart(2, '0'); |
|
169 |
- const minutes = String(now.getMinutes()).padStart(2, '0'); |
|
170 |
- const seconds = String(now.getSeconds()).padStart(2, '0'); |
|
171 |
- return `${hours}:${minutes}:${seconds}`; |
|
172 |
- }, |
|
173 |
- getCategoryClass(category) { |
|
174 |
- switch (category) { |
|
175 |
- case '용역': return 'category-service'; |
|
176 |
- case '내부': return 'category-internal'; |
|
177 |
- case '국가과제': return 'category-government'; |
|
178 |
- default: return ''; |
|
146 |
+ //사용자 정보 전체 조회 |
|
147 |
+ async searchUsers() { |
|
148 |
+ try { |
|
149 |
+ const response = await findUsersProc(this.searchReqDTO); |
|
150 |
+ if (response.status === 200) { |
|
151 |
+ this.selectedUsers = response.data.data.users; // API 응답에서 카테고리 목록을 가져옴 |
|
152 |
+ console.log("user~~~~~!", this.selectedUsers); |
|
153 |
+ } |
|
154 |
+ } catch (error) { |
|
155 |
+ console.error("검색 중 오류 발생:", error); |
|
179 | 156 |
} |
180 | 157 |
}, |
158 |
+ selectUser(item) { |
|
159 |
+ this.selectedUser = { |
|
160 |
+ file: { // file 객체도 함께 초기화 |
|
161 |
+ fileNm: "기본이미지", |
|
162 |
+ filePath: this.photoicon, |
|
163 |
+ }, |
|
164 |
+ loginId: item.loginId, |
|
165 |
+ userNm: item.userNm, |
|
166 |
+ deptNm: item.deptNm, |
|
167 |
+ clsfNm: item.clsfNm, |
|
168 |
+ rspofcNm: item.rspofcNm, |
|
169 |
+ mbtlnum: item.mbtlnum, |
|
170 |
+ brthdy: item.brthdy, |
|
171 |
+ encpn: item.encpn, |
|
172 |
+ }; |
|
173 |
+ // 썸네일 정보가 있으면 업데이트 |
|
174 |
+ if (item.thumbnail && Object.keys(item.thumbnail).length > 0) { |
|
175 |
+ this.selectedUser.file.fileNm = item.thumbnail.fileNm; // thumbnail -> item.thumbnail |
|
176 |
+ this.selectedUser.file.filePath = item.thumbnail.filePath; // thumbnail -> item.thumbnail |
|
177 |
+ } |
|
178 |
+ } |
|
181 | 179 |
}, |
182 | 180 |
watch: { |
183 | 181 |
|
... | ... | @@ -186,10 +184,7 @@ |
186 | 184 |
|
187 | 185 |
}, |
188 | 186 |
mounted() { |
189 |
- console.log('main mounted'); |
|
190 |
- setInterval(() => { |
|
191 |
- this.time = this.getCurrentTime(); |
|
192 |
- }, 1000); |
|
187 |
+ this.searchUsers(); |
|
193 | 188 |
} |
194 | 189 |
} |
195 | 190 |
</script> |
... | ... | @@ -197,4 +192,11 @@ |
197 | 192 |
tr { |
198 | 193 |
cursor: pointer; |
199 | 194 |
} |
195 |
+.inactive-user { |
|
196 |
+ color: #999999 !important; /* 회색 텍스트 */ |
|
197 |
+} |
|
198 |
+ |
|
199 |
+.inactive-user td { |
|
200 |
+ color: #999999 !important; /* 모든 td 요소를 회색으로 */ |
|
201 |
+} |
|
200 | 202 |
</style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/User/MyPage.vue
+++ client/views/pages/User/MyPage.vue
... | ... | @@ -4,19 +4,23 @@ |
4 | 4 |
<div class="name-box"> |
5 | 5 |
<div class="img-area"> |
6 | 6 |
<div class="img"> |
7 |
- <img :src="previewImg || placeholder" alt="미리보기 이미지" /> |
|
7 |
+ <img :src="croppedImg || placeholder" alt="미리보기 이미지" /> |
|
8 | 8 |
<button class="close-btn" @click="removeImage">×</button> |
9 | 9 |
</div> |
10 | 10 |
<div class="info"> |
11 | 11 |
<div class="file"> |
12 | 12 |
<label for="fileUpload" class="file-label"> |
13 | 13 |
<img :src="file" alt=""> |
14 |
- <p>업로드</p> |
|
14 |
+ <button @click="showCropModal = true"> |
|
15 |
+ <p>업로드</p> |
|
16 |
+ </button> |
|
15 | 17 |
</label> |
16 |
- <input id="fileUpload" type="file" @change="handleFileUpload" accept="image/*" /> |
|
17 | 18 |
</div> |
18 | 19 |
</div> |
19 | 20 |
</div> |
21 |
+ <ImageCropper :key="cropperKey" field="avatar" v-model="showCropModal" :width="200" :height="200" |
|
22 |
+ img-format="png" lang-type="ko" @crop-success="cropSuccess" :no-square="true" :round-cropping="true" |
|
23 |
+ :max-size="1024" /> |
|
20 | 24 |
</div> |
21 | 25 |
</div> |
22 | 26 |
</div> |
... | ... | @@ -24,7 +28,6 @@ |
24 | 28 |
|
25 | 29 |
<div class="d-flex justify-content-center py-4"> |
26 | 30 |
<a href="index.html" class="logo d-flex align-items-center w-auto"> |
27 |
- <!-- <span class="d-none d-lg-block"> <img :src="logo" alt=""></span> --> |
|
28 | 31 |
</a> |
29 | 32 |
</div><!-- End Logo --> |
30 | 33 |
|
... | ... | @@ -35,72 +38,70 @@ |
35 | 38 |
<form class="row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }" |
36 | 39 |
@submit.prevent="handleRegister" novalidate> |
37 | 40 |
<div class="col-12"> |
38 |
- <label for="yourName" class="form-label">아이디</label> |
|
39 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly |
|
40 |
- placeholder="admin"> |
|
41 |
+ <label for="loginId" class="form-label">아이디</label> |
|
42 |
+ <input v-model="selectedUser.loginId" type="text" name="name" class="form-control" id="yourName" required |
|
43 |
+ readonly placeholder="admin"> |
|
41 | 44 |
</div> |
42 | 45 |
<div class="col-12 "> |
43 | 46 |
<div class="col-12 border-x"> |
44 |
- <label for="youremail" class="form-label "> |
|
47 |
+ <label for="userNm" class="form-label "> |
|
45 | 48 |
<p>이름 |
46 | 49 |
<p class="require"><img :src="require" alt=""></p> |
47 | 50 |
</p> |
48 | 51 |
</label> |
49 |
- <input v-model="email" type="text" name="username" class="form-control" id="youremail" required> |
|
52 |
+ <input v-model="requestDTO.userNm" type="text" name="username" class="form-control" id="youremail" |
|
53 |
+ required> |
|
50 | 54 |
</div> |
51 | 55 |
|
52 | 56 |
<div class="col-12 border-x"> |
53 |
- <label for="yourPassword" class="form-label">부서</label> |
|
54 |
- <input v-model="password" type="password" name="password" class="form-control" id="yourPassword" required |
|
55 |
- readonly placeholder="주식회사 테이큰 소프트"> |
|
57 |
+ <label for="deptNm" class="form-label">부서</label> |
|
58 |
+ <input v-model="selectedUser.deptNm" type="password" name="password" class="form-control" |
|
59 |
+ id="yourPassword" required readonly placeholder="부서"> |
|
56 | 60 |
</div> |
57 | 61 |
</div> |
58 | 62 |
<div class="col-12"> |
59 | 63 |
<div class="col-12 border-x"> |
60 |
- <label for="youremail" class="form-label">직급</label> |
|
61 |
- <input v-model="email" type="text" name="username" class="form-control" id="youremail" required readonly |
|
62 |
- placeholder="과장"> |
|
64 |
+ <label for="clsfNm" class="form-label">직급</label> |
|
65 |
+ <input v-model="selectedUser.clsfNm" type="text" name="username" class="form-control" id="youremail" |
|
66 |
+ required readonly placeholder="직급"> |
|
63 | 67 |
</div> |
64 | 68 |
|
65 | 69 |
<div class="col-12 border-x"> |
66 |
- <label for="yourPassword" class="form-label">직책</label> |
|
67 |
- <input v-model="password" type="password" name="password" class="form-control" id="yourPassword" required |
|
68 |
- readonly placeholder="팀장"> |
|
70 |
+ <label for="rspofcNm" class="form-label">직책</label> |
|
71 |
+ <input v-model="selectedUser.rspofcNm" type="password" name="password" class="form-control" |
|
72 |
+ id="yourPassword" required readonly placeholder="직책"> |
|
69 | 73 |
</div> |
70 | 74 |
</div> |
71 | 75 |
<div class="col-12"> |
72 |
- <label for="yourName" class="form-label">연락처</label> |
|
73 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required> |
|
76 |
+ <label for="mbtlnum" class="form-label">연락처</label> |
|
77 |
+ <input v-model="requestDTO.mbtlnum" type="text" name="name" class="form-control" id="yourName" required> |
|
74 | 78 |
</div> |
75 | 79 |
<div class="col-12"> |
76 |
- <label for="yourName" class="form-label">생년월일</label> |
|
77 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required> |
|
80 |
+ <label for="brthdy" class="form-label">생년월일</label> |
|
81 |
+ <input v-model="requestDTO.brthdy" type="text" name="name" class="form-control" id="yourName" required> |
|
78 | 82 |
</div> |
79 | 83 |
<div class="col-12"> |
80 | 84 |
<label for="yourName" class="form-label">입사일</label> |
81 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required readonly |
|
82 |
- placeholder="2025-01-01"> |
|
85 |
+ <input v-model="requestDTO.encpn" type="text" name="name" class="form-control" id="yourName" required |
|
86 |
+ readonly placeholder="2025-01-01"> |
|
83 | 87 |
</div> |
84 | 88 |
<div class="col-12"> |
85 |
- <label for="yourName" class="form-label">현재 비밀번호</label> |
|
86 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required> |
|
89 |
+ <label for="oldPassword" class="form-label">현재 비밀번호</label> |
|
90 |
+ <input v-model="updatePasswordDTO.oldPassword" type="password" name="name" class="form-control" |
|
91 |
+ id="yourName" required> |
|
87 | 92 |
</div> |
88 | 93 |
<div class="col-12"> |
89 |
- <label for="yourName" class="form-label">새 비밀번호</label> |
|
94 |
+ <label for="newPassword" class="form-label">새 비밀번호</label> |
|
90 | 95 |
<div class="box"> |
91 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required> |
|
96 |
+ <input v-model="updatePasswordDTO.newPassword" type="password" name="name" class="form-control" |
|
97 |
+ id="yourName" required> |
|
92 | 98 |
<div class="invalid-feedback">※ 비밀번호는 6~12자의 영문자와 숫자, 특수기호조합으로 작성해주세요.</div> |
93 | 99 |
</div> |
94 | 100 |
</div> |
95 |
- <div class="col-12 border-x"> |
|
96 |
- <label for="yourName" class="form-label">연락처</label> |
|
97 |
- <input v-model="name" type="text" name="name" class="form-control" id="yourName" required> |
|
98 |
- </div> |
|
99 |
- |
|
100 | 101 |
</form> |
101 | 102 |
<div class="buttons"> |
102 |
- <button class="btn sm btn-red " type="submit">회원탈퇴</button> |
|
103 |
- <button class="btn sm secondary " type="submit">수정</button> |
|
103 |
+ <button class="btn sm btn-red " type="submit" @clcick="submitDeleteForm">회원탈퇴</button> |
|
104 |
+ <button class="btn sm secondary " type="submit" @click="submitUpdateForm">수정</button> |
|
104 | 105 |
</div> |
105 | 106 |
</div> |
106 | 107 |
</div> |
... | ... | @@ -109,6 +110,9 @@ |
109 | 110 |
</template> |
110 | 111 |
|
111 | 112 |
<script> |
113 |
+import ImageCropper from 'vue-image-crop-upload'; //이미지 자르기기 |
|
114 |
+import { findUsersDetProc, updateUsersProc, updatePassword } from "../../../resources/api/user"; |
|
115 |
+ |
|
112 | 116 |
export default { |
113 | 117 |
data() { |
114 | 118 |
return { |
... | ... | @@ -116,80 +120,308 @@ |
116 | 120 |
placeholder: "/client/resources/img/img1.png", |
117 | 121 |
require: "/client/resources/img/require.png", |
118 | 122 |
file: "/client/resources/img/file.png", |
119 |
- name: '', |
|
120 |
- email: '', |
|
121 |
- password: '', |
|
122 |
- dept: '', |
|
123 |
- level: '', |
|
124 | 123 |
acceptTerms: false, |
125 | 124 |
formSubmitted: false, |
125 |
+ userData: null, |
|
126 |
+ croppedImg: null, // 최종 크롭되어 표시될 이미지의 Data URL 또는 서버 이미지 URL |
|
127 |
+ cropperKey: 0, |
|
128 |
+ showCropModal: false, // ImageCropper 모달을 보여줄지 말지 결정하는 변수 |
|
129 |
+ selectedUser: {}, //내정보 |
|
130 |
+ requestDTO: { |
|
131 |
+ loginId: null, // @NotBlank, @Size, @Pattern |
|
132 |
+ clsf: null, // 선택된 직급의 코드 (예: "RESP_JRM") |
|
133 |
+ rspofc: null, // 선택된 직책의 코드 (예: "RSP_DR") |
|
134 |
+ userNm: null, // @NotBlank, @Size |
|
135 |
+ password: null, // @NotBlank, @Size, @Pattern (이 화면에서 password 받는다면) |
|
136 |
+ mbtlnum: null, // 휴대폰 |
|
137 |
+ telno: null, // 전화번호 (현재 화면에 없으면 null 유지) |
|
138 |
+ email: null, // 이메일 (현재 화면에 없으면 null 유지) |
|
139 |
+ zip: null, // 우편번호 (현재 화면에 없으면 null 유지) |
|
140 |
+ adres: null, // 주소 (현재 화면에 없으면 null 유지) |
|
141 |
+ detailAdres: null, // 상세주소 (현재 화면에 없으면 null 유지) |
|
142 |
+ fileId: null, // 파일 아이디 (파일 저장 후 백엔드에서 반환될 값, 직접 입력X) |
|
143 |
+ brthdy: null, // 생년월일 (YYYY-MM-DD 문자열) |
|
144 |
+ encpn: null, // 입사일 (YYYY-MM-DD 문자열) |
|
145 |
+ authorList: [], // 선택된 권한의 ID들을 담을 배열 (UserAuthorVO 리스트 형태로 변환하여 전송) |
|
146 |
+ }, |
|
147 |
+ updatePasswordDTO: { |
|
148 |
+ oldPassword: "", |
|
149 |
+ newPassword: "", |
|
150 |
+ resetAt: false, |
|
151 |
+ }, |
|
126 | 152 |
}; |
127 | 153 |
}, |
154 |
+ components: { |
|
155 |
+ ImageCropper, findUsersDetProc, updateUsersProc, updatePassword |
|
156 |
+ }, |
|
128 | 157 |
methods: { |
129 |
- handleFileUpload(event) { |
|
130 |
- const file = event.target.files[0]; |
|
131 |
- if (file && file.type.startsWith('image/')) { |
|
132 |
- const reader = new FileReader(); |
|
133 |
- reader.onload = (e) => { |
|
134 |
- this.previewImg = e.target.result; // 파일 읽기 완료 후 미리보기 이미지 설정 |
|
135 |
- }; |
|
136 |
- reader.readAsDataURL(file); // 파일을 데이터 URL로 읽기 |
|
137 |
- } else { |
|
138 |
- alert('이미지 파일만 선택할 수 있습니다.'); |
|
158 |
+ dataURLtoFile(dataurl, filename) { |
|
159 |
+ var arr = dataurl.split(','), |
|
160 |
+ mime = arr[0].match(/:(.*?);/)[1], |
|
161 |
+ bstr = atob(arr[1]), |
|
162 |
+ n = bstr.length, |
|
163 |
+ u8arr = new Uint8Array(n); |
|
164 |
+ while (n--) { |
|
165 |
+ u8arr[n] = bstr.charCodeAt(n); |
|
139 | 166 |
} |
167 |
+ return new File([u8arr], filename, { type: mime }); |
|
140 | 168 |
}, |
169 |
+ //사용자 정보 상세 조회 |
|
170 |
+ async searchUser() { |
|
171 |
+ try { |
|
172 |
+ const response = await findUsersDetProc(this.userData.userId); |
|
173 |
+ if (response.status === 200) { |
|
174 |
+ this.selectedUser = response.data.data.user; // API 응답에서 카테고리 목록을 가져옴 |
|
141 | 175 |
|
142 |
- // 이미지 삭제 함수 |
|
143 |
- removeImage() { |
|
144 |
- this.previewImg = null; // 미리보기 이미지 삭제 |
|
145 |
- this.$refs.fileUpload.value = null; // 파일 input 초기화 |
|
146 |
- }, |
|
147 |
- handleRegister() { |
|
148 |
- this.formSubmitted = true; |
|
149 |
- // 이메일과 비밀번호가 빈 값이 아니어야 한다 |
|
150 |
- if (!this.email.includes('@')) { |
|
151 |
- alert('이메일은 @를 포함해야 합니다.'); |
|
152 |
- return; // Stop further processing if email is invalid |
|
153 |
- } |
|
176 |
+ this.requestDTO = { |
|
177 |
+ userNm: this.selectedUser.userNm, |
|
178 |
+ clsf: this.selectedUser.clsf, |
|
179 |
+ rspofc: this.selectedUser.rspofc || '', |
|
180 |
+ mbtlnum: this.selectedUser.mbtlnum, |
|
181 |
+ telno: this.selectedUser.telno, |
|
182 |
+ email: this.selectedUser.email, |
|
183 |
+ zip: this.selectedUser.zip, |
|
184 |
+ adres: this.selectedUser.adres, |
|
185 |
+ detailAdres: this.selectedUser.detailAdres, |
|
186 |
+ brthdy: this.selectedUser.brthdy, |
|
187 |
+ encpn: this.selectedUser.encpn, |
|
188 |
+ userSttus: this.selectedUser.userSttus, |
|
189 |
+ useAt: this.selectedUser.useAt, |
|
190 |
+ fileId: this.selectedUser.fileId, // 기존 파일 ID 보존 |
|
191 |
+ authorList: this.selectedUser.authorList |
|
192 |
+ } |
|
193 |
+ this.croppedImg = this.selectedUser.thumbnail?.filePath || null; |
|
154 | 194 |
|
155 |
- console.log('Email:', this.email); |
|
156 |
- console.log('Password:', this.password); |
|
157 |
- console.log('Name:', this.name); |
|
158 |
- console.log('Accept Terms:', this.acceptTerms); |
|
159 |
- if (this.email && this.password && this.name && this.acceptTerms && this.dept && this.level) { |
|
160 |
- // 로컬 스토리지에 회원가입 정보 저장 |
|
161 |
- const userData = { |
|
162 |
- name: this.name, |
|
163 |
- email: this.email, |
|
164 |
- password: this.password, |
|
165 |
- dept: this.dept, |
|
166 |
- level: this.level, |
|
167 |
- }; |
|
168 |
- |
|
169 |
- console.log('User Data to be saved:', userData); |
|
170 |
- try { |
|
171 |
- localStorage.setItem("UserInfo", JSON.stringify(userData)); |
|
172 |
- alert('회원가입이 완료되었습니다!'); |
|
173 |
- |
|
174 |
- // Redirect to login page |
|
175 |
- this.$router.push("/login"); |
|
176 |
- } catch (error) { |
|
177 |
- console.error("Error saving to localStorage:", error); |
|
178 |
- alert("회원가입 중 오류가 발생했습니다."); |
|
195 |
+ console.log("유저 아이디", this.selectedUser); |
|
179 | 196 |
} |
180 |
- } else { |
|
181 |
- alert("모든 필드를 입력해주세요."); |
|
197 |
+ } catch (error) { |
|
198 |
+ console.error("검색 중 오류 발생:", error); |
|
199 |
+ } |
|
200 |
+ }, |
|
201 |
+ //이미지 지우기 |
|
202 |
+ removeImage() { |
|
203 |
+ this.croppedImg = null; |
|
204 |
+ this.cropperKey++; |
|
205 |
+ }, |
|
206 |
+ //이미지 저장 |
|
207 |
+ cropSuccess(imgDataUrl) { |
|
208 |
+ this.croppedImg = imgDataUrl; |
|
209 |
+ this.showCropModal = false; |
|
210 |
+ }, |
|
211 |
+ //회원탈퇴 |
|
212 |
+ async submitDeleteForm() { |
|
213 |
+ const confirmDelete = confirm("정말로 사용자를 탈퇴시키겠습니까?\n이 작업은 되돌릴 수 없습니다."); |
|
214 |
+ |
|
215 |
+ if (!confirmDelete) { // 사용자가 '아니오'를 눌렀을 때 |
|
216 |
+ alert("사용자 탈퇴가 취소되었습니다."); |
|
217 |
+ return; // 함수 실행 중단 |
|
218 |
+ } |
|
219 |
+ |
|
220 |
+ // 2. FormData 객체 생성 |
|
221 |
+ const formData = new FormData(); |
|
222 |
+ |
|
223 |
+ // 수정용 DTO 생성 (기존 데이터 + 수정 데이터) |
|
224 |
+ const updateDTO = { |
|
225 |
+ userNm: this.selectedUser.userNm, |
|
226 |
+ useAt: "N", |
|
227 |
+ fileId: this.selectedUser.fileId, |
|
228 |
+ }; |
|
229 |
+ |
|
230 |
+ formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], { |
|
231 |
+ type: 'application/json' |
|
232 |
+ })); |
|
233 |
+ |
|
234 |
+ // 4. API 통신 |
|
235 |
+ try { |
|
236 |
+ const response = await updateUsersProc(this.$route.query.id, formData); |
|
237 |
+ |
|
238 |
+ if (response.status === 200) { |
|
239 |
+ alert('사용자 탈퇴퇴가 성공적으로 저장되었습니다!'); |
|
240 |
+ } |
|
241 |
+ } catch (error) { |
|
242 |
+ console.error("사용자 정보 저장 중 오류 발생:", error); |
|
243 |
+ alert("사용자 정보 저장 중 오류가 발생했습니다."); |
|
244 |
+ if (error.response && error.response.data && error.response.data.message) { |
|
245 |
+ alert(`오류: ${error.response.data.message}`); |
|
246 |
+ } |
|
247 |
+ } |
|
248 |
+ }, |
|
249 |
+ //수정용 |
|
250 |
+ async submitUpdateForm() { |
|
251 |
+ // 1. 기본 유효성 검사 |
|
252 |
+ if (this.$isEmpty(this.requestDTO.userNm)) { |
|
253 |
+ alert("이름을 입력해 주세요."); |
|
254 |
+ return; |
|
255 |
+ } |
|
256 |
+ |
|
257 |
+ const oldPwEmpty = this.$isEmpty(this.updatePasswordDTO.oldPassword); |
|
258 |
+ const newPwEmpty = this.$isEmpty(this.updatePasswordDTO.newPassword); |
|
259 |
+ |
|
260 |
+ // 2. 비밀번호 입력 상황별 분기 처리 |
|
261 |
+ if (!oldPwEmpty && newPwEmpty) { |
|
262 |
+ alert("새 비밀번호를 입력해주세요."); |
|
263 |
+ return; |
|
264 |
+ } |
|
265 |
+ |
|
266 |
+ if (oldPwEmpty && !newPwEmpty) { |
|
267 |
+ alert("기존 비밀번호를 입력해주세요."); |
|
268 |
+ return; |
|
269 |
+ } |
|
270 |
+ |
|
271 |
+ if (oldPwEmpty && newPwEmpty) { |
|
272 |
+ const confirmUpdate = confirm("비밀번호 변경 없이 내 정보를 수정하시겠습니까?"); |
|
273 |
+ if (!confirmUpdate) { |
|
274 |
+ return; // 취소 시 함수 종료 |
|
275 |
+ } |
|
276 |
+ } |
|
277 |
+ console.log("비밀번호 변경 여부:", !oldPwEmpty && !newPwEmpty); |
|
278 |
+ |
|
279 |
+ // 2. FormData 객체 생성 |
|
280 |
+ const formData = new FormData(); |
|
281 |
+ |
|
282 |
+ const updateDTO = { |
|
283 |
+ ...this.requestDTO, |
|
284 |
+ }; |
|
285 |
+ |
|
286 |
+ formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], { |
|
287 |
+ type: 'application/json' |
|
288 |
+ })); |
|
289 |
+ |
|
290 |
+ // 3. 파일 처리 |
|
291 |
+ // 기존 파일이 그대로 유지되는 경우 (fileId만 전송) |
|
292 |
+ if (this.selectedUser.fileId && !this.croppedImg.startsWith('data:')) { |
|
293 |
+ // 기존 파일 ID 유지 |
|
294 |
+ updateDTO.fileId = this.selectedUser.fileId; |
|
295 |
+ } |
|
296 |
+ // 새로운 파일이 업로드된 경우 |
|
297 |
+ else if (this.croppedImg && this.croppedImg.startsWith('data:')) { |
|
298 |
+ const profileImageFile = this.dataURLtoFile(this.croppedImg, 'profile_image.png'); |
|
299 |
+ formData.append("multipartFiles", profileImageFile); |
|
300 |
+ // 기존 파일 ID 제거 (새 파일로 교체) |
|
301 |
+ updateDTO.fileId = null; |
|
302 |
+ } |
|
303 |
+ |
|
304 |
+ // 4. API 통신 |
|
305 |
+ try { |
|
306 |
+ const response = await updateUsersProc(this.userData.userId, formData); |
|
307 |
+ |
|
308 |
+ if (response.status === 200) { |
|
309 |
+ if (!oldPwEmpty && !newPwEmpty) { |
|
310 |
+ await this.resetPassword(); |
|
311 |
+ } else { |
|
312 |
+ alert('사용자 정보가 성공적으로 저장되었습니다!'); |
|
313 |
+ } |
|
314 |
+ } |
|
315 |
+ } catch (error) { |
|
316 |
+ console.error("사용자 정보 저장 중 오류 발생:", error); |
|
317 |
+ alert("사용자 정보 저장 중 오류가 발생했습니다."); |
|
318 |
+ if (error.response && error.response.data && error.response.data.message) { |
|
319 |
+ alert(`오류: ${error.response.data.message}`); |
|
320 |
+ } |
|
321 |
+ } |
|
322 |
+ }, |
|
323 |
+ |
|
324 |
+ async resetPassword() { |
|
325 |
+ try { |
|
326 |
+ const response = await updatePassword(this.userData.userId, this.updatePasswordDTO); |
|
327 |
+ if (response.status === 200) { |
|
328 |
+ alert("내 정보가 수정되었습니다."); |
|
329 |
+ } |
|
330 |
+ } catch (error) { |
|
331 |
+ console.error("검색 중 오류 발생:", error); |
|
182 | 332 |
} |
183 | 333 |
}, |
184 | 334 |
}, |
185 | 335 |
created() { |
186 |
- // 로컬스토리지에서 데이터 불러오기 |
|
187 |
- const storedData = localStorage.getItem('DeptData'); |
|
336 |
+ const storedData = localStorage.getItem('vuex'); // 'vuex' 키로 바꿔야 함 |
|
188 | 337 |
if (storedData) { |
189 |
- this.DeptData = JSON.parse(storedData); |
|
338 |
+ const parsedData = JSON.parse(storedData); |
|
339 |
+ // vuex 스토어 구조에 따라 userInfo 경로 찾아야 함 |
|
340 |
+ if (parsedData && parsedData.userInfo) { |
|
341 |
+ this.userData = parsedData.userInfo; |
|
342 |
+ console.log("userData 확인:", this.userData); |
|
343 |
+ } else { |
|
344 |
+ console.log("vuex 스토어에 userInfo가 없습니다."); |
|
345 |
+ } |
|
346 |
+ } else { |
|
347 |
+ console.log("로컬스토리지에 vuex 데이터가 없습니다."); |
|
190 | 348 |
} |
349 |
+ }, |
|
350 |
+ mounted() { |
|
351 |
+ this.searchUser(); |
|
191 | 352 |
}, |
192 | 353 |
}; |
193 | 354 |
</script> |
194 | 355 |
|
195 |
-<style scoped></style>(파일 끝에 줄바꿈 문자 없음) |
|
356 |
+<style scoped> |
|
357 |
+.profile-container { |
|
358 |
+ padding: 20px; |
|
359 |
+ text-align: center; |
|
360 |
+} |
|
361 |
+ |
|
362 |
+.info { |
|
363 |
+ margin-bottom: 20px; |
|
364 |
+} |
|
365 |
+ |
|
366 |
+.profile-image-area { |
|
367 |
+ width: 150px; |
|
368 |
+ height: 150px; |
|
369 |
+ border-radius: 50%; |
|
370 |
+ /* 원형으로 만들게 */ |
|
371 |
+ overflow: hidden; |
|
372 |
+ /* 영역 밖으로 넘치는 이미지 자르기 */ |
|
373 |
+ margin: 0 auto 20px; |
|
374 |
+ /* 가운데 정렬 및 아래 여백 */ |
|
375 |
+ border: 2px solid #eee; |
|
376 |
+ /* 테두리 */ |
|
377 |
+ display: flex; |
|
378 |
+ align-items: center; |
|
379 |
+ justify-content: center; |
|
380 |
+ background-color: #f9f9f9; |
|
381 |
+ /* 기본 배경색 */ |
|
382 |
+} |
|
383 |
+ |
|
384 |
+.cropped-profile-img { |
|
385 |
+ width: 100%; |
|
386 |
+ height: 100%; |
|
387 |
+ object-fit: cover; |
|
388 |
+ /* 이미지를 영역에 꽉 채우되 비율 유지 */ |
|
389 |
+} |
|
390 |
+ |
|
391 |
+.default-profile-icon { |
|
392 |
+ font-size: 60px; |
|
393 |
+ color: #ccc; |
|
394 |
+ display: flex; |
|
395 |
+ align-items: center; |
|
396 |
+ justify-content: center; |
|
397 |
+ width: 100%; |
|
398 |
+ height: 100%; |
|
399 |
+} |
|
400 |
+ |
|
401 |
+.remove-btn { |
|
402 |
+ display: inline-block; |
|
403 |
+ /* 버튼 형태로 보이기 위해 */ |
|
404 |
+ padding: 10px 20px; |
|
405 |
+ margin: 5px; |
|
406 |
+ background-color: #007bff; |
|
407 |
+ color: white; |
|
408 |
+ border: none; |
|
409 |
+ border-radius: 5px; |
|
410 |
+ cursor: pointer; |
|
411 |
+ font-size: 16px; |
|
412 |
+ transition: background-color 0.3s ease; |
|
413 |
+} |
|
414 |
+ |
|
415 |
+.file-label.upload-btn:hover, |
|
416 |
+.remove-btn:hover { |
|
417 |
+ background-color: #0056b3; |
|
418 |
+} |
|
419 |
+ |
|
420 |
+.remove-btn { |
|
421 |
+ background-color: #dc3545; |
|
422 |
+} |
|
423 |
+ |
|
424 |
+.remove-btn:hover { |
|
425 |
+ background-color: #c82333; |
|
426 |
+} |
|
427 |
+</style>(파일 끝에 줄바꿈 문자 없음) |
--- package-lock.json
+++ package-lock.json
... | ... | @@ -27,6 +27,7 @@ |
27 | 27 |
"simple-datatables": "^9.2.1", |
28 | 28 |
"url-loader": "4.1.1", |
29 | 29 |
"vue": "3.5.13", |
30 |
+ "vue-image-crop-upload": "^3.0.3", |
|
30 | 31 |
"vue-loader": "^17.0.0", |
31 | 32 |
"vue-router": "4.1.5", |
32 | 33 |
"vue-style-loader": "4.1.3", |
... | ... | @@ -2460,6 +2461,22 @@ |
2460 | 2461 |
"url": "https://github.com/sponsors/sindresorhus" |
2461 | 2462 |
} |
2462 | 2463 |
}, |
2464 |
+ "node_modules/babel-runtime": { |
|
2465 |
+ "version": "6.26.0", |
|
2466 |
+ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", |
|
2467 |
+ "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", |
|
2468 |
+ "license": "MIT", |
|
2469 |
+ "dependencies": { |
|
2470 |
+ "core-js": "^2.4.0", |
|
2471 |
+ "regenerator-runtime": "^0.11.0" |
|
2472 |
+ } |
|
2473 |
+ }, |
|
2474 |
+ "node_modules/babel-runtime/node_modules/regenerator-runtime": { |
|
2475 |
+ "version": "0.11.1", |
|
2476 |
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", |
|
2477 |
+ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", |
|
2478 |
+ "license": "MIT" |
|
2479 |
+ }, |
|
2463 | 2480 |
"node_modules/balanced-match": { |
2464 | 2481 |
"version": "1.0.2", |
2465 | 2482 |
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", |
... | ... | @@ -3090,6 +3107,14 @@ |
3090 | 3107 |
"version": "1.0.6", |
3091 | 3108 |
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", |
3092 | 3109 |
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", |
3110 |
+ "license": "MIT" |
|
3111 |
+ }, |
|
3112 |
+ "node_modules/core-js": { |
|
3113 |
+ "version": "2.6.12", |
|
3114 |
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", |
|
3115 |
+ "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", |
|
3116 |
+ "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.", |
|
3117 |
+ "hasInstallScript": true, |
|
3093 | 3118 |
"license": "MIT" |
3094 | 3119 |
}, |
3095 | 3120 |
"node_modules/core-util-is": { |
... | ... | @@ -7787,6 +7812,15 @@ |
7787 | 7812 |
} |
7788 | 7813 |
} |
7789 | 7814 |
}, |
7815 |
+ "node_modules/vue-image-crop-upload": { |
|
7816 |
+ "version": "3.0.3", |
|
7817 |
+ "resolved": "https://registry.npmjs.org/vue-image-crop-upload/-/vue-image-crop-upload-3.0.3.tgz", |
|
7818 |
+ "integrity": "sha512-VeBsU0oI1hXeCvdpnu19DM/r3KTlI8SUXTxsHsU4MhDXR0ahRziiL9tf4FbILGx+gRVNZhGbl32yuM6TiaGNhA==", |
|
7819 |
+ "license": "ISC", |
|
7820 |
+ "dependencies": { |
|
7821 |
+ "babel-runtime": "^6.11.6" |
|
7822 |
+ } |
|
7823 |
+ }, |
|
7790 | 7824 |
"node_modules/vue-loader": { |
7791 | 7825 |
"version": "17.4.2", |
7792 | 7826 |
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-17.4.2.tgz", |
--- package.json
+++ package.json
... | ... | @@ -22,6 +22,7 @@ |
22 | 22 |
"simple-datatables": "^9.2.1", |
23 | 23 |
"url-loader": "4.1.1", |
24 | 24 |
"vue": "3.5.13", |
25 |
+ "vue-image-crop-upload": "^3.0.3", |
|
25 | 26 |
"vue-loader": "^17.0.0", |
26 | 27 |
"vue-router": "4.1.5", |
27 | 28 |
"vue-style-loader": "4.1.3", |
--- webpack.config.js
+++ webpack.config.js
... | ... | @@ -13,6 +13,13 @@ |
13 | 13 |
app: `${BASE_DIR}/client/views/index.js`, |
14 | 14 |
}, |
15 | 15 |
|
16 |
+ resolve: { |
|
17 |
+ alias: { |
|
18 |
+ '@': path.resolve(__dirname, 'client/'), |
|
19 |
+ }, |
|
20 |
+ extensions: ['.js', '.vue', '.json'], |
|
21 |
+ }, |
|
22 |
+ |
|
16 | 23 |
module: { |
17 | 24 |
rules: [ |
18 | 25 |
{ |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?