
--- client/resources/api/user.js
+++ client/resources/api/user.js
... | ... | @@ -1,5 +1,4 @@ |
1 | 1 |
import { apiClient, fileClient } from "./index"; |
2 |
-import uploadService from "@/resources/js/uploadService"; |
|
3 | 2 |
|
4 | 3 |
// 조회 - 목록 |
5 | 4 |
export const findUsersProc = data => { |
--- client/resources/js/downloadService.js
... | ... | @@ -1,257 +0,0 @@ |
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
... | ... | @@ -1,133 +0,0 @@ |
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
... | ... | @@ -1,109 +0,0 @@ |
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
... | ... | @@ -1,267 +0,0 @@ |
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/component/Popup/HrPopup.vue
+++ client/views/component/Popup/HrPopup.vue
... | ... | @@ -7,7 +7,8 @@ |
7 | 7 |
<div class="sch-form-wrap"> |
8 | 8 |
<div class="input-group"> |
9 | 9 |
<div class="sch-input"> |
10 |
- <input type="text" class="form-control" placeholder="직원명" v-model="request.searchText" @keyup.enter="findDatas"> |
|
10 |
+ <input type="text" class="form-control" placeholder="직원명" v-model="request.searchText" |
|
11 |
+ @keyup.enter="findDatas"> |
|
11 | 12 |
<button type="button" class="ico-sch" @click="findDatas"> |
12 | 13 |
<SearchOutlined /> |
13 | 14 |
</button> |
... | ... | @@ -32,7 +33,8 @@ |
32 | 33 |
<td>{{ item.deptNm }}</td> |
33 | 34 |
<td>{{ item.userNm }}</td> |
34 | 35 |
<td> |
35 |
- <button type="button" class="btn sm sm secondary" @click="selectPerson(item)" :disabled="isUserSelected(item.userId)">선택</button> |
|
36 |
+ <button type="button" class="btn sm sm secondary" @click="selectPerson(item)" |
|
37 |
+ :disabled="isUserSelected(item.userId)">선택</button> |
|
36 | 38 |
</td> |
37 | 39 |
</tr> |
38 | 40 |
</tbody> |
... | ... | @@ -81,7 +83,7 @@ |
81 | 83 |
|
82 | 84 |
computed: { |
83 | 85 |
selectedUserIds() { |
84 |
- return new Set(this.lists.map(sanctn => sanctn.confmerId)); |
|
86 |
+ return new Set(this.lists.map(member => member.userId)); // userId로 변경 |
|
85 | 87 |
} |
86 | 88 |
}, |
87 | 89 |
|
... | ... | @@ -121,6 +123,7 @@ |
121 | 123 |
// 승인자 선택 |
122 | 124 |
selectPerson(item) { |
123 | 125 |
this.$emit('onSelected', item); |
126 |
+ this.$emit('close'); |
|
124 | 127 |
}, |
125 | 128 |
|
126 | 129 |
// 페이지 이동 |
--- client/views/layout/Menu.vue
+++ client/views/layout/Menu.vue
... | ... | @@ -21,7 +21,7 @@ |
21 | 21 |
</li> |
22 | 22 |
<i class="fas fa-bars"></i> |
23 | 23 |
<li class="nav-item"> |
24 |
- <router-link to="/hr-management" class="nav-link " active-class="active"><span>인사관리</span></router-link> |
|
24 |
+ <router-link to="/Hrm-management" class="nav-link " active-class="active"><span>인사관리</span></router-link> |
|
25 | 25 |
</li> |
26 | 26 |
<i class="fas fa-bars"></i> |
27 | 27 |
<li class="nav-item"> |
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
... | ... | @@ -46,11 +46,11 @@ |
46 | 46 |
import CardInfoManagement from '../pages/Manager/asset/CardInfoManagement.vue'; |
47 | 47 |
|
48 | 48 |
//인사관리 |
49 |
-import hrSearch from '../pages/Manager/hr/hrSearch.vue'; |
|
50 |
-import hrManagement from '../pages/Manager/hr/hrManagement.vue'; |
|
51 |
-import hrDetail from '../pages/Manager/hr/hrDetail.vue'; |
|
52 |
-import hrInsert from '../pages/Manager/hr/hrInsert.vue'; |
|
53 |
-import buseoManagement from '../pages/Manager/hr/buseoManagement.vue'; |
|
49 |
+import HrmSearch from '../pages/Manager/hrm/HrmSearch.vue'; |
|
50 |
+import HrmManagement from '../pages/Manager/hrm/HrmManagement.vue'; |
|
51 |
+import HrmDetail from '../pages/Manager/hrm/HrmDetail.vue'; |
|
52 |
+import HrmInsert from '../pages/Manager/hrm/HrmInsert.vue'; |
|
53 |
+import TeamManagement from '../pages/Manager/hrm/TeamManagement.vue'; |
|
54 | 54 |
|
55 | 55 |
//시스템관리 |
56 | 56 |
import AuthorManagementComp from '../pages/Manager/system/AuthorManagement.vue'; |
... | ... | @@ -127,13 +127,13 @@ |
127 | 127 |
}, |
128 | 128 |
// 인사관리 |
129 | 129 |
{ |
130 |
- path: '/hr-management', name: 'hr', redirect: '/hr-management.page/hrSearch.page', |
|
130 |
+ path: '/hrm-management', name: 'hrm', redirect: '/Hrm-management.page/HrmSearch.page', |
|
131 | 131 |
children: [ |
132 |
- { path: 'hrSearch.page', name: 'hrSearch', component: hrSearch }, |
|
133 |
- { path: 'hrManagement.page', name: 'hrManagement', component: hrManagement }, |
|
134 |
- { path: 'hrDetail.page', name: 'hrDetail', component: hrDetail }, |
|
135 |
- { path: 'hrInsert.page', name: 'hrInsert', component: hrInsert }, |
|
136 |
- { path: 'buseoManagement.page', name: 'buseoManagement', component: buseoManagement }, |
|
132 |
+ { path: 'HrmSearch.page', name: 'HrmSearch', component: HrmSearch }, |
|
133 |
+ { path: 'HrmManagement.page', name: 'HrmManagement', component: HrmManagement }, |
|
134 |
+ { path: 'HrmDetail.page', name: 'HrmDetail', component: HrmDetail }, |
|
135 |
+ { path: 'HrmInsert.page', name: 'HrmInsert', component: HrmInsert }, |
|
136 |
+ { path: 'TeamManagement.page', name: 'TeamManagement', component: TeamManagement }, |
|
137 | 137 |
] |
138 | 138 |
}, |
139 | 139 |
// 시스템관리 |
--- client/views/pages/Manager/hr/buseoManagement.vue
... | ... | @@ -1,331 +0,0 @@ |
1 | -<template> | |
2 | - <div class="card"> | |
3 | - <div class="card-body"> | |
4 | - <h2 class="card-title">부서 관리</h2> | |
5 | - <!-- Multi Columns Form --> | |
6 | - <div class="flex align-top"> | |
7 | - <div class="sch-form-wrap search "> | |
8 | - <div v-for="(menu, index) in menus" :key="index" class="sidemenu"> | |
9 | - <details class="menu-box" open> | |
10 | - <summary class="topmenu"> | |
11 | - <img :src="arrow" alt="" class="arrow"> | |
12 | - <img :src="topmenuicon" alt=""> | |
13 | - <p>{{ menu.title }} </p> | |
14 | - <button @click="addSubMenu(index)" class="btn sm xsm secondary">sub +</button> | |
15 | - </summary> | |
16 | - <ul> | |
17 | - <li class="submenu" v-for="(submenu, subIndex) in menu.submenus" :key="subIndex"> | |
18 | - <router-link :to="submenu.link" exact-active-class="active-link" v-slot="{ isExactActive }"> | |
19 | - <img :src="menuicon" alt=""> | |
20 | - <p>{{ submenu.label }}</p> | |
21 | - </router-link> | |
22 | - </li> | |
23 | - </ul> | |
24 | - </details> | |
25 | - </div> | |
26 | - <div class="buttons"> | |
27 | - <button @click="addTopMenu"><img :src="addtopmenu" alt=""></button> | |
28 | - | |
29 | - </div> | |
30 | - </div> | |
31 | - <div style="width: 100%;"> | |
32 | - <div class=" sch-form-wrap title-wrap"> | |
33 | - <h3><img :src="h3icon" alt="">부서 정보</h3> | |
34 | - <div class="buttons" style="margin: 0;"> | |
35 | - <button type="submit" class="btn sm sm tertiary">초기화</button> | |
36 | - <button type="reset" class="btn sm sm secondary">등록</button> | |
37 | - <button type="delete" class="btn sm sm btn-red">삭제</button> | |
38 | - </div> | |
39 | - </div> | |
40 | - <form class="row g-3 pt-3 needs-validation detail" @submit.prevent="handleSubmit" | |
41 | - style="margin-bottom: 3rem;"> | |
42 | - <div class="col-12"> | |
43 | - <label for="purpose" class="form-label">상위부서</label> | |
44 | - <input type="text" class="form-control" id="purpose" v-model="purpose" readonly /> | |
45 | - </div> | |
46 | - <div class="col-12"> | |
47 | - <label for="purpose" class="form-label"> | |
48 | - <p>부서명 | |
49 | - <p class="require"><img :src="require" alt=""></p> | |
50 | - </p> | |
51 | - </label> | |
52 | - <input type="text" class="form-control" id="purpose" v-model="purpose" /> | |
53 | - </div> | |
54 | - | |
55 | - <div class="col-12 chuljang border-x"> | |
56 | - <label for="prvonsh" class="form-label">부서설명</label> | |
57 | - <input type="text" class="form-control textarea" id="reason" v-model="reason" /> | |
58 | - </div> | |
59 | - | |
60 | - | |
61 | - </form> | |
62 | - <div class=" sch-form-wrap title-wrap"> | |
63 | - <h3><img :src="h3icon" alt="">부서 사용자</h3> | |
64 | - <div class="buttons" style="margin: 0;"> | |
65 | - <button type="reset" class="btn sm sm secondary" @click="showPopup = true">사용자 추가</button> | |
66 | - <button type="delete" class="btn sm sm btn-red" @click="removeMember(index)">사용자 삭제</button> | |
67 | - </div> | |
68 | - <HrPopup v-if="showPopup" @close="showPopup = false" @select="addMember"/> | |
69 | - </div> | |
70 | - <div class="tbl-wrap chk-area"> | |
71 | - <table id="myTable" class="tbl data"> | |
72 | - | |
73 | - <thead> | |
74 | - <tr> | |
75 | - <th>선택</th> | |
76 | - <th>직급</th> | |
77 | - <th>이름</th> | |
78 | - <th>부서장</th> | |
79 | - </tr> | |
80 | - </thead> | |
81 | - <!-- 동적으로 <td> 생성 --> | |
82 | - <tbody> | |
83 | - <tr v-for="(member, index) in members" :key="index"> | |
84 | - <td> | |
85 | - <div class="form-check"> | |
86 | - <input type="checkbox" :id="`chk_${index}`" :value="member.name" v-model="member.checked" /> | |
87 | - <label :for="`chk_${index}`"></label> | |
88 | - </div> | |
89 | - </td> | |
90 | - <td>{{ member.position }}</td> | |
91 | - <td>{{ member.name }}</td> | |
92 | - <td> | |
93 | - <div class="form-check"> | |
94 | - <input type="radio" name="manager" :id="`rdo_${index}`" :value="member.name" | |
95 | - v-model="selectedManager" /> | |
96 | - <label :for="`rdo_${index}`"></label> | |
97 | - </div> | |
98 | - </td> | |
99 | - </tr> | |
100 | - </tbody> | |
101 | - </table> | |
102 | - | |
103 | - </div> | |
104 | - <div class="pagination"> | |
105 | - <ul> | |
106 | - <!-- 왼쪽 화살표 (이전 페이지) --> | |
107 | - <li class="arrow" :class="{ disabled: currentPage === 1 }" @click="changePage(currentPage - 1)"> | |
108 | - < | |
109 | - </li> | |
110 | - | |
111 | - <!-- 페이지 번호 --> | |
112 | - <li v-for="page in totalPages" :key="page" :class="{ active: currentPage === page }" | |
113 | - @click="changePage(page)"> | |
114 | - {{ page }} | |
115 | - </li> | |
116 | - | |
117 | - <!-- 오른쪽 화살표 (다음 페이지) --> | |
118 | - <li class="arrow" :class="{ disabled: currentPage === totalPages }" @click="changePage(currentPage + 1)"> | |
119 | - > | |
120 | - </li> | |
121 | - </ul> | |
122 | - </div> | |
123 | - </div> | |
124 | - </div> | |
125 | - | |
126 | - | |
127 | - </div> | |
128 | - </div> | |
129 | -</template> | |
130 | - | |
131 | -<script> | |
132 | -import { ref } from 'vue'; | |
133 | -import { PlusCircleFilled, CloseOutlined, DownOutlined } from '@ant-design/icons-vue'; | |
134 | -import HrPopup from '../../../component/Popup/HrPopup.vue'; | |
135 | -const isOpen = ref(false) | |
136 | -export default { | |
137 | - data() { | |
138 | - const today = new Date().toISOString().split('T')[0]; | |
139 | - return { | |
140 | - selectedManager: '', | |
141 | - showPopup: false, | |
142 | - menus: [ | |
143 | - { | |
144 | - title: '부서1', | |
145 | - submenus: [ | |
146 | - { | |
147 | - label: '직원검색', | |
148 | - link: { name: 'hrSearch' }, | |
149 | - }, | |
150 | - ], | |
151 | - }, | |
152 | - ], | |
153 | - currentPage: 1, | |
154 | - totalPages: 3, | |
155 | - members: [] , | |
156 | - selectedManager: null, | |
157 | - h3icon: "/client/resources/img/h3icon.png", | |
158 | - require: "/client/resources/img/require.png", | |
159 | - menuicon: "/client/resources/img/arrow-rg.png", | |
160 | - topmenuicon: "/client/resources/img/topmenu.png", | |
161 | - arrow: "/client/resources/img/arrow.png", | |
162 | - addtopmenu: "/client/resources/img/addtopmenu.png", | |
163 | - addsubmenu: "/client/resources/img/addsubmenu.png", | |
164 | - fileName: '', | |
165 | - startDate: today, | |
166 | - startTime: '09:00', | |
167 | - endDate: today, | |
168 | - endTime: '18:00', | |
169 | - where: '', | |
170 | - purpose: '', | |
171 | - approvals: [ | |
172 | - { | |
173 | - category: '결재', | |
174 | - name: '', | |
175 | - }, | |
176 | - ], | |
177 | - receipts: [ | |
178 | - { | |
179 | - type: '개인결제', | |
180 | - category: '결재', | |
181 | - category1: '구분', | |
182 | - }, | |
183 | - ], | |
184 | - }; | |
185 | - }, | |
186 | - components: { | |
187 | - PlusCircleFilled, CloseOutlined, DownOutlined, HrPopup | |
188 | - }, | |
189 | - computed: { | |
190 | - loginUser() { | |
191 | - const authStore = useAuthStore(); | |
192 | - return authStore.getLoginUser; | |
193 | - }, | |
194 | - }, | |
195 | - | |
196 | - methods: { | |
197 | - addMember(selectedUser) { | |
198 | - this.members.push({ | |
199 | - position: selectedUser.position, | |
200 | - name: selectedUser.name, // or other fields if needed | |
201 | - }); | |
202 | - this.showPopup = false; // 팝업 닫기 | |
203 | - }, | |
204 | - removeMember() { | |
205 | - this.members = this.members.filter(member => !member.checked); | |
206 | - }, | |
207 | - addTopMenu() { | |
208 | - const newIndex = this.menus.length + 1; | |
209 | - this.menus.push({ | |
210 | - title: `부서${newIndex}`, | |
211 | - submenus: [], | |
212 | - }); | |
213 | - }, | |
214 | - addSubMenu(menuIndex) { | |
215 | - this.menus[menuIndex].submenus.push({ | |
216 | - label: `신규메뉴${this.menus[menuIndex].submenus.length + 1}`, | |
217 | - link: { name: 'hrSearch' }, | |
218 | - }); | |
219 | - }, | |
220 | - goToPage(type) { | |
221 | - if (type === '회의록 등록') { | |
222 | - this.$router.push({ name: 'meetingInsert' }); | |
223 | - } else if (type === '출장') { | |
224 | - this.$router.push({ name: 'ChuljangDetail' }); | |
225 | - } | |
226 | - }, | |
227 | - handleFileUpload(event) { | |
228 | - const file = event.target.files[0]; | |
229 | - if (file) { | |
230 | - this.fileName = file.name; | |
231 | - } | |
232 | - }, | |
233 | - addApproval() { | |
234 | - this.approvals.push({ | |
235 | - category: '결재', | |
236 | - name: '', | |
237 | - }); | |
238 | - }, | |
239 | - addReceipt() { | |
240 | - this.receipts.push({ | |
241 | - type: '개인결제', | |
242 | - category: '결재', | |
243 | - category1: '', | |
244 | - name: '', | |
245 | - file: null, | |
246 | - }); | |
247 | - }, | |
248 | - // 승인자 삭제 | |
249 | - removeApproval(index) { | |
250 | - this.approvals.splice(index, 1); | |
251 | - }, | |
252 | - removeReceipt(index) { | |
253 | - this.receipts.splice(index, 1); | |
254 | - }, | |
255 | - validateForm() { | |
256 | - // 필수 입력 필드 체크 | |
257 | - if ( | |
258 | - this.startDate && | |
259 | - this.startTime && | |
260 | - this.endDate && | |
261 | - this.endTime && | |
262 | - this.where && | |
263 | - this.purpose.trim() !== "" | |
264 | - ) { | |
265 | - this.isFormValid = true; | |
266 | - } else { | |
267 | - this.isFormValid = false; | |
268 | - } | |
269 | - }, | |
270 | - calculateDayCount() { | |
271 | - const start = new Date(`${this.startDate}T${this.startTime}:00`); | |
272 | - const end = new Date(`${this.endDate}T${this.endTime}:00`); | |
273 | - | |
274 | - let totalHours = (end - start) / (1000 * 60 * 60); // 밀리초를 시간 단위로 변환 | |
275 | - | |
276 | - if (this.startDate !== this.endDate) { | |
277 | - // 시작일과 종료일이 다른경우 | |
278 | - const startDateObj = new Date(this.startDate); | |
279 | - const endDateObj = new Date(this.endDate); | |
280 | - const daysDifference = (endDateObj - startDateObj) / (1000 * 60 * 60 * 24); // 두 날짜 사이의 차이를 일수로 계산 | |
281 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
282 | - this.dayCount = daysDifference + 0.5; // 시간 조건이 기준에서 벗어날 경우 | |
283 | - } else { | |
284 | - this.dayCount = Math.ceil(daysDifference + 1); // 시간 조건이 기준에 맞을 경우 | |
285 | - } | |
286 | - } else { | |
287 | - // 시작일과 종료일이 같은 경우 | |
288 | - if (this.startTime !== "09:00" || this.endTime !== "18:00") { | |
289 | - this.dayCount = 0.5; // 시작 시간 또는 종료 시간이 기준과 다를 경우 0.5 | |
290 | - } else { | |
291 | - this.dayCount = 1; // 기준 시간(09:00~18:00)이 맞으면 1일로 간주 | |
292 | - } | |
293 | - } | |
294 | - | |
295 | - this.validateForm(); // dayCount 변경 후 폼 재검증 | |
296 | - }, | |
297 | - handleSubmit() { | |
298 | - this.validateForm(); // 제출 시 유효성 확인 | |
299 | - if (this.isFormValid) { | |
300 | - localStorage.setItem('ChuljangFormData', JSON.stringify(this.$data)); | |
301 | - alert("승인 요청이 완료되었습니다."); | |
302 | - // 추가 처리 로직 (API 요청 등) | |
303 | - } else { | |
304 | - alert("모든 필드를 올바르게 작성해주세요."); | |
305 | - } | |
306 | - }, | |
307 | - loadFormData() { | |
308 | - const savedData = localStorage.getItem('ChuljangFormData'); | |
309 | - if (savedData) { | |
310 | - this.$data = JSON.parse(savedData); | |
311 | - } | |
312 | - }, | |
313 | - }, | |
314 | - mounted() { | |
315 | - // Load the saved form data when the page is loaded | |
316 | - this.loadFormData(); | |
317 | - }, | |
318 | - watch: { | |
319 | - startDate: 'calculateDayCount', | |
320 | - startTime: 'calculateDayCount', | |
321 | - endDate: 'calculateDayCount', | |
322 | - endTime: 'calculateDayCount', | |
323 | - where: 'validateForm', | |
324 | - purpose: "validateForm", | |
325 | - }, | |
326 | -}; | |
327 | -</script> | |
328 | - | |
329 | -<style scoped> | |
330 | -/* 필요한 스타일 추가 */ | |
331 | -</style> |
--- client/views/pages/Manager/hr/hr.vue
... | ... | @@ -1,99 +0,0 @@ |
1 | -<template> | |
2 | - <div class="sidemenu"> | |
3 | - <div class="myinfo simple"> | |
4 | - <div class="name-box"> | |
5 | - <div class="img-area"> | |
6 | - <div><img :src="photoicon" alt=""> | |
7 | - <p class="name">OOO과장</p> | |
8 | - </div> | |
9 | - <div class="info"> | |
10 | - <p>솔루션 개발팀</p> | |
11 | - <i class="fa-bars"></i> | |
12 | - <p>팀장</p> | |
13 | - </div> | |
14 | - </div> | |
15 | - </div> | |
16 | - | |
17 | - | |
18 | - <details class="menu-box" open> | |
19 | - <summary><p>직원</p><div class="icon"><img :src="topmenuicon" alt=""></div></summary> | |
20 | - <ul> | |
21 | - <li> <router-link :to="{ name: 'hrSearch' }" exact-active-class="active-link" v-slot="{ isExactActive }"> | |
22 | - <p>직원검색</p> | |
23 | - <div class="icon" v-if="isExactActive"> | |
24 | - <img :src="menuicon" alt=""> | |
25 | - </div> | |
26 | - </router-link></li> | |
27 | - <li> | |
28 | - <router-link :to="{ name: 'hrManagement' }" exact-active-class="active-link" v-slot="{ isExactActive }"> | |
29 | - <p>직원관리</p> | |
30 | - <div class="icon" v-if="isExactActive"> | |
31 | - <img :src="menuicon" alt=""> | |
32 | - </div> | |
33 | - </router-link> | |
34 | - </li> | |
35 | - | |
36 | - | |
37 | - </ul> | |
38 | - </details> | |
39 | - <details class="menu-box"> | |
40 | - <summary><p>부서</p><div class="icon"><img :src="topmenuicon" alt=""></div></summary> | |
41 | - <ul> | |
42 | - <li> <router-link :to="{ name: 'buseoManagement' }" exact-active-class="active-link" v-slot="{ isExactActive }"> | |
43 | - <p>부서관리</p> | |
44 | - <div class="icon" v-if="isExactActive"> | |
45 | - <img :src="menuicon" alt=""> | |
46 | - </div> | |
47 | - </router-link></li> | |
48 | - | |
49 | - | |
50 | - </ul> | |
51 | - </details> | |
52 | - </div> | |
53 | - </div> | |
54 | - <!-- End Page Title --> | |
55 | - <div class="content"> | |
56 | - <router-view></router-view> | |
57 | - | |
58 | - </div> | |
59 | -</template> | |
60 | - | |
61 | -<script> | |
62 | -import { ref } from 'vue'; | |
63 | - | |
64 | -export default { | |
65 | - data() { | |
66 | - return { | |
67 | - photoicon: "/client/resources/img/photo_icon.png", | |
68 | - menuicon: "/client/resources/img/menuicon.png", | |
69 | - topmenuicon: "/client/resources/img/topmenuicon.png", | |
70 | - // 데이터 초기화 | |
71 | - years: [2023, 2024, 2025], // 연도 목록 | |
72 | - months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록 | |
73 | - selectedYear: '', | |
74 | - selectedMonth: '', | |
75 | - DeptData: [ | |
76 | - { member: '', deptNM: '', acceptTerms: false }, | |
77 | - // 더 많은 데이터 추가... | |
78 | - ], | |
79 | - filteredData: [], | |
80 | - }; | |
81 | - }, | |
82 | - computed: { | |
83 | - }, | |
84 | - methods: { | |
85 | - | |
86 | - // 페이지 변경 | |
87 | - changePage(page) { | |
88 | - this.currentPage = page; | |
89 | - }, | |
90 | - }, | |
91 | - created() { | |
92 | - }, | |
93 | - mounted() { | |
94 | - | |
95 | - }, | |
96 | -}; | |
97 | -</script> | |
98 | - | |
99 | -<style scoped></style> |
--- client/views/pages/Manager/hr/hrDetail.vue
+++ client/views/pages/Manager/hrm/HrmDetail.vue
... | ... | @@ -118,7 +118,7 @@ |
118 | 118 |
} |
119 | 119 |
}, |
120 | 120 |
goToPage() { |
121 |
- this.$router.push({ name: 'hrManagement' }); |
|
121 |
+ this.$router.push({ name: 'HrmManagement' }); |
|
122 | 122 |
}, |
123 | 123 |
goToDetail() { |
124 | 124 |
this.$router.push({ |
... | ... | @@ -167,7 +167,7 @@ |
167 | 167 |
const confirmDelete = confirm("정말로 사용자를 복구시키겠습니까?\n이 작업은 되돌릴 수 없습니다."); |
168 | 168 |
|
169 | 169 |
if (!confirmDelete) { // 사용자가 '아니오'를 눌렀을 때 |
170 |
- alert("사용자 복구구가 취소되었습니다."); |
|
170 |
+ alert("사용자 복구가 취소되었습니다."); |
|
171 | 171 |
return; // 함수 실행 중단 |
172 | 172 |
} |
173 | 173 |
|
--- client/views/pages/Manager/hr/hrInsert.vue
+++ client/views/pages/Manager/hrm/HrmInsert.vue
... | ... | @@ -149,7 +149,6 @@ |
149 | 149 |
import BuseoPopup from "../../../component/Popup/BuseoPopup.vue"; |
150 | 150 |
import { findAuthorsProc } from "../../../../resources/api/author"; |
151 | 151 |
import ImageCropper from 'vue-image-crop-upload'; //이미지 자르기기 |
152 |
-import FileUploadProgress from '@/views/component/FileUploadProgress.vue'; |
|
153 | 152 |
import { joinProc, findUsersDetProc, updateUsersProc } from "../../../../resources/api/user"; |
154 | 153 |
import { findCodesProc } from "../../../../resources/api/code"; |
155 | 154 |
|
... | ... | @@ -225,7 +224,7 @@ |
225 | 224 |
} |
226 | 225 |
}, |
227 | 226 |
components: { |
228 |
- SearchOutlined, PlusCircleFilled, BuseoPopup, CloseOutlined, findAuthorsProc, ImageCropper, FileUploadProgress, joinProc, findCodesProc |
|
227 |
+ SearchOutlined, PlusCircleFilled, BuseoPopup, CloseOutlined, findAuthorsProc, ImageCropper, joinProc, findCodesProc |
|
229 | 228 |
}, |
230 | 229 |
created() { |
231 | 230 |
if (this.$route.query.id != null) { |
... | ... | @@ -267,7 +266,7 @@ |
267 | 266 |
}, |
268 | 267 |
// 취소 누를 시 직원관리 페이지로 이동 |
269 | 268 |
goToManagementList() { |
270 |
- this.$router.push('/hr-management/hrManagement.page'); |
|
269 |
+ this.$router.push('/hrm-management/HrmManagement.page'); |
|
271 | 270 |
}, |
272 | 271 |
// 공통코드 직급 |
273 | 272 |
async clsfTypeCodes() { |
... | ... | @@ -370,7 +369,6 @@ |
370 | 369 |
|
371 | 370 |
if (response.status === 200) { |
372 | 371 |
alert('사용자 정보가 성공적으로 저장되었습니다!'); |
373 |
- // 성공 시 페이지 이동 (예: this.$router.push('/hr-management/hrManagement.page')) |
|
374 | 372 |
} |
375 | 373 |
} catch (error) { |
376 | 374 |
console.error("사용자 정보 저장 중 오류 발생:", error); |
... | ... | @@ -433,7 +431,6 @@ |
433 | 431 |
|
434 | 432 |
if (response.status === 200) { |
435 | 433 |
alert('사용자 정보가 성공적으로 저장되었습니다!'); |
436 |
- // 성공 시 페이지 이동 (예: this.$router.push('/hr-management/hrManagement.page')) |
|
437 | 434 |
} |
438 | 435 |
} catch (error) { |
439 | 436 |
console.error("사용자 정보 저장 중 오류 발생:", error); |
--- client/views/pages/Manager/hr/hrManagement.vue
+++ client/views/pages/Manager/hrm/HrmManagement.vue
... | ... | @@ -186,11 +186,11 @@ |
186 | 186 |
}, |
187 | 187 |
goToDetailPage(item) { |
188 | 188 |
// item.id 또는 다른 식별자를 사용하여 URL을 구성할 수 있습니다. |
189 |
- this.$router.push({ name: 'hrDetail', query: { id: item.userId } }); |
|
189 |
+ this.$router.push({ name: 'HrmDetail', query: { id: item.userId } }); |
|
190 | 190 |
}, |
191 | 191 |
goToPage(type) { |
192 | 192 |
if (type === '등록') { |
193 |
- this.$router.push({ name: 'hrInsert' }); |
|
193 |
+ this.$router.push({ name: 'HrmInsert' }); |
|
194 | 194 |
} |
195 | 195 |
}, |
196 | 196 |
}, |
--- client/views/pages/Manager/hr/hrSearch.vue
+++ client/views/pages/Manager/hrm/HrmSearch.vue
No changes |
+++ client/views/pages/Manager/hrm/TeamManagement.vue
... | ... | @@ -0,0 +1,644 @@ |
1 | +<template> | |
2 | + <div class="card"> | |
3 | + <div class="card-body"> | |
4 | + <h2 class="card-title">부서 관리</h2> | |
5 | + <!-- Multi Columns Form --> | |
6 | + <div class="flex align-top"> | |
7 | + <div class="sch-form-wrap search"> | |
8 | + <div v-for="(menu, index) in menus" :key="index" class="sidemenu"> | |
9 | + <div class="menu-box"> | |
10 | + <div class="topmenu" @click="toggleMenu(index)"> | |
11 | + <img :src="arrow" alt="" class="arrow" :class="{ 'arrow-open': menu.isOpen }"> | |
12 | + <img :src="topmenuicon" alt=""> | |
13 | + <p @click.stop="selectTopDepartment(menu.title)" | |
14 | + :class="{ | |
15 | + 'active-top': selectedDepartment?.name === menu.title && selectedDepartment?.isTopLevel | |
16 | + }" | |
17 | + class="top-dept-name">{{ menu.title }}</p> | |
18 | + <button @click.stop="addSubMenu(index)" class="btn sm xsm secondary">sub +</button> | |
19 | + </div> | |
20 | + <ul v-show="menu.isOpen" class="submenu-list"> | |
21 | + <li class="submenu" v-for="(submenu, subIndex) in menu.submenus" :key="subIndex"> | |
22 | + <div @click="selectDepartment(menu.title, submenu.label)" | |
23 | + :class="{ | |
24 | + 'active-link': selectedDepartment?.parent === menu.title && selectedDepartment?.name === submenu.label && !selectedDepartment?.isTopLevel | |
25 | + }" | |
26 | + class="submenu-item"> | |
27 | + <img :src="menuicon" alt=""> | |
28 | + <p>{{ submenu.label }}</p> | |
29 | + </div> | |
30 | + </li> | |
31 | + </ul> | |
32 | + </div> | |
33 | + </div> | |
34 | + <div class="buttons"> | |
35 | + <button @click="addTopMenu"><img :src="addtopmenu" alt=""></button> | |
36 | + </div> | |
37 | + </div> | |
38 | + | |
39 | + <div style="width: 100%;"> | |
40 | + <div class="sch-form-wrap title-wrap"> | |
41 | + <h3><img :src="h3icon" alt="">부서 정보</h3> | |
42 | + <div class="buttons" style="margin: 0;"> | |
43 | + <button type="button" class="btn sm sm tertiary">초기화</button> | |
44 | + <button type="button" class="btn sm sm secondary">등록</button> | |
45 | + <button type="button" class="btn sm sm btn-red">삭제</button> | |
46 | + </div> | |
47 | + </div> | |
48 | + | |
49 | + <form class="row g-3 pt-3 needs-validation detail" @submit.prevent="handleSubmit" style="margin-bottom: 3rem;"> | |
50 | + <div class="col-12"> | |
51 | + <label for="parentDept" class="form-label">상위부서</label> | |
52 | + <input type="text" class="form-control" id="parentDept" v-model="departmentInfo.parentDept" readonly /> | |
53 | + </div> | |
54 | + | |
55 | + <div class="col-12"> | |
56 | + <label for="deptName" class="form-label"> | |
57 | + <p>부서명 | |
58 | + <span class="require"><img :src="require" alt=""></span> | |
59 | + </p> | |
60 | + </label> | |
61 | + <input type="text" class="form-control" id="deptName" v-model="departmentInfo.deptName" /> | |
62 | + </div> | |
63 | + | |
64 | + <div class="col-12 chuljang border-x"> | |
65 | + <label for="deptDesc" class="form-label">부서설명</label> | |
66 | + <textarea class="form-control textarea" id="deptDesc" v-model="departmentInfo.deptDesc" rows="3"></textarea> | |
67 | + </div> | |
68 | + </form> | |
69 | + | |
70 | + <div class="sch-form-wrap title-wrap"> | |
71 | + <h3><img :src="h3icon" alt="">부서 사용자</h3> | |
72 | + <div class="buttons" style="margin: 0;"> | |
73 | + <button type="button" class="btn sm sm secondary" @click="showPopup = true">사용자 추가</button> | |
74 | + <button type="button" class="btn sm sm btn-red" @click="removeMember()">사용자 삭제</button> | |
75 | + </div> | |
76 | + <HrPopup v-if="showPopup" | |
77 | + :lists="members" | |
78 | + @close="showPopup = false" | |
79 | + @onSelected="addMember"/> | |
80 | + </div> | |
81 | + | |
82 | + <div class="tbl-wrap chk-area"> | |
83 | + <table id="myTable" class="tbl data"> | |
84 | + <thead> | |
85 | + <tr> | |
86 | + <th>선택</th> | |
87 | + <th>직급</th> | |
88 | + <th>직책</th> | |
89 | + <th>부서</th> | |
90 | + <th>이름</th> | |
91 | + <th>부서장</th> | |
92 | + </tr> | |
93 | + </thead> | |
94 | + <tbody> | |
95 | + <tr v-for="(member, index) in members" :key="index"> | |
96 | + <td> | |
97 | + <div class="form-check"> | |
98 | + <input type="checkbox" :id="`chk_${index}`" :value="member.name" v-model="member.checked" /> | |
99 | + <label :for="`chk_${index}`"></label> | |
100 | + </div> | |
101 | + </td> | |
102 | + <td>{{ member.position }}</td> | |
103 | + <td>{{ member.responsibility }}</td> | |
104 | + <td>{{ member.department }}</td> | |
105 | + <td>{{ member.name }}</td> | |
106 | + <td> | |
107 | + <div class="form-check"> | |
108 | + <input type="radio" name="manager" :id="`rdo_${index}`" :value="member.userId" v-model="selectedManager" /> | |
109 | + <label :for="`rdo_${index}`"></label> | |
110 | + </div> | |
111 | + </td> | |
112 | + </tr> | |
113 | + </tbody> | |
114 | + </table> | |
115 | + </div> | |
116 | + | |
117 | + <div class="pagination"> | |
118 | + <ul> | |
119 | + <li class="arrow" :class="{ disabled: currentPage === 1 }" @click="changePage(currentPage - 1)"> | |
120 | + < | |
121 | + </li> | |
122 | + <li v-for="page in totalPages" :key="page" :class="{ active: currentPage === page }" @click="changePage(page)"> | |
123 | + {{ page }} | |
124 | + </li> | |
125 | + <li class="arrow" :class="{ disabled: currentPage === totalPages }" @click="changePage(currentPage + 1)"> | |
126 | + > | |
127 | + </li> | |
128 | + </ul> | |
129 | + </div> | |
130 | + </div> | |
131 | + </div> | |
132 | + </div> | |
133 | + </div> | |
134 | +</template> | |
135 | + | |
136 | +<script> | |
137 | +import { ref } from 'vue'; | |
138 | +import { PlusCircleFilled, CloseOutlined, DownOutlined } from '@ant-design/icons-vue'; | |
139 | +import HrPopup from '../../../component/Popup/HrPopup.vue'; | |
140 | + | |
141 | +const isOpen = ref(false); | |
142 | + | |
143 | +export default { | |
144 | + data() { | |
145 | + const today = new Date().toISOString().split('T')[0]; | |
146 | + return { | |
147 | + selectedManager: '', | |
148 | + showPopup: false, | |
149 | + selectedDepartment: null, | |
150 | + departmentInfo: { | |
151 | + parentDept: '', | |
152 | + deptName: '', | |
153 | + deptDesc: '' | |
154 | + }, | |
155 | + menus: [ | |
156 | + { | |
157 | + title: '부서1', | |
158 | + isOpen: true, | |
159 | + submenus: [ | |
160 | + { | |
161 | + label: '직원검색', | |
162 | + link: { name: 'hrSearch' }, | |
163 | + }, | |
164 | + ], | |
165 | + }, | |
166 | + ], | |
167 | + currentPage: 1, | |
168 | + totalPages: 3, | |
169 | + members: [], | |
170 | + h3icon: "/client/resources/img/h3icon.png", | |
171 | + require: "/client/resources/img/require.png", | |
172 | + menuicon: "/client/resources/img/arrow-rg.png", | |
173 | + topmenuicon: "/client/resources/img/topmenu.png", | |
174 | + arrow: "/client/resources/img/arrow.png", | |
175 | + addtopmenu: "/client/resources/img/addtopmenu.png", | |
176 | + addsubmenu: "/client/resources/img/addsubmenu.png", | |
177 | + }; | |
178 | + }, | |
179 | + | |
180 | + components: { | |
181 | + PlusCircleFilled, | |
182 | + CloseOutlined, | |
183 | + DownOutlined, | |
184 | + HrPopup | |
185 | + }, | |
186 | + | |
187 | + methods: { | |
188 | + toggleMenu(index) { | |
189 | + this.menus[index].isOpen = !this.menus[index].isOpen; | |
190 | + }, | |
191 | + | |
192 | + selectTopDepartment(deptName) { | |
193 | + this.selectedDepartment = { | |
194 | + parent: null, // 상위부서 없음 | |
195 | + name: deptName, | |
196 | + isTopLevel: true // 최상위 부서임을 표시 | |
197 | + }; | |
198 | + | |
199 | + this.departmentInfo.parentDept = ''; // 상위부서 비움 | |
200 | + this.departmentInfo.deptName = deptName; | |
201 | + }, | |
202 | + | |
203 | + selectDepartment(parentTitle, deptName) { | |
204 | + this.selectedDepartment = { | |
205 | + parent: parentTitle, | |
206 | + name: deptName, | |
207 | + isTopLevel: false | |
208 | + }; | |
209 | + | |
210 | + this.departmentInfo.parentDept = parentTitle; | |
211 | + this.departmentInfo.deptName = deptName; | |
212 | + }, | |
213 | + | |
214 | + addMember(selectedUser) { | |
215 | + console.log('addMember 호출됨:', selectedUser); // 디버깅용 | |
216 | + | |
217 | + // 이미 추가된 사용자인지 확인 | |
218 | + const isAlreadyAdded = this.members.some(member => member.userId === selectedUser.userId); | |
219 | + | |
220 | + if (isAlreadyAdded) { | |
221 | + alert('이미 추가된 사용자입니다.'); | |
222 | + this.showPopup = false; | |
223 | + return; | |
224 | + } | |
225 | + | |
226 | + this.members.push({ | |
227 | + userId: selectedUser.userId, | |
228 | + position: selectedUser.clsfNm || selectedUser.position, // 직급 | |
229 | + name: selectedUser.userNm || selectedUser.name, // 이름 | |
230 | + department: selectedUser.deptNm, // 부서 | |
231 | + responsibility: selectedUser.rspofcNm, // 직책 | |
232 | + checked: false | |
233 | + }); | |
234 | + | |
235 | + this.showPopup = false; | |
236 | + console.log('새로운 멤버 추가됨, 총 멤버 수:', this.members.length); // 디버깅용 | |
237 | + }, | |
238 | + | |
239 | + removeMember() { | |
240 | + this.members = this.members.filter(member => !member.checked); | |
241 | + }, | |
242 | + | |
243 | + addTopMenu() { | |
244 | + const newIndex = this.menus.length + 1; | |
245 | + this.menus.push({ | |
246 | + title: `부서${newIndex}`, | |
247 | + isOpen: false, | |
248 | + submenus: [], | |
249 | + }); | |
250 | + }, | |
251 | + | |
252 | + addSubMenu(menuIndex) { | |
253 | + this.menus[menuIndex].submenus.push({ | |
254 | + label: `신규메뉴${this.menus[menuIndex].submenus.length + 1}`, | |
255 | + link: { name: 'hrSearch' }, | |
256 | + }); | |
257 | + }, | |
258 | + | |
259 | + changePage(page) { | |
260 | + if (page >= 1 && page <= this.totalPages) { | |
261 | + this.currentPage = page; | |
262 | + } | |
263 | + }, | |
264 | + | |
265 | + handleSubmit() { | |
266 | + console.log('폼 제출:', this.departmentInfo); | |
267 | + }, | |
268 | + }, | |
269 | + | |
270 | + mounted() { | |
271 | + // 초기에 첫 번째 하위부서 선택 | |
272 | + if (this.menus.length > 0 && this.menus[0].submenus.length > 0) { | |
273 | + this.selectDepartment(this.menus[0].title, this.menus[0].submenus[0].label); | |
274 | + } | |
275 | + } | |
276 | +}; | |
277 | +</script> | |
278 | + | |
279 | +<style scoped> | |
280 | +.card { | |
281 | + background: white; | |
282 | + border-radius: 8px; | |
283 | + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); | |
284 | + padding: 24px; | |
285 | + margin: 20px; | |
286 | +} | |
287 | + | |
288 | +.card-title { | |
289 | + font-size: 24px; | |
290 | + font-weight: bold; | |
291 | + color: #333; | |
292 | + margin-bottom: 24px; | |
293 | + border-bottom: 2px solid #f0f0f0; | |
294 | + padding-bottom: 12px; | |
295 | +} | |
296 | + | |
297 | +.flex { | |
298 | + display: flex; | |
299 | + gap: 24px; | |
300 | +} | |
301 | + | |
302 | +.align-top { | |
303 | + align-items: flex-start; | |
304 | +} | |
305 | + | |
306 | +.sch-form-wrap.search { | |
307 | + min-width: 280px; | |
308 | + background: #f8f9fa; | |
309 | + border-radius: 8px; | |
310 | + padding: 16px; | |
311 | + border: 1px solid #e9ecef; | |
312 | +} | |
313 | + | |
314 | +.sidemenu { | |
315 | + margin-bottom: 12px; | |
316 | +} | |
317 | + | |
318 | +.menu-box { | |
319 | + background: white; | |
320 | + border-radius: 6px; | |
321 | + border: 1px solid #dee2e6; | |
322 | + overflow: hidden; | |
323 | +} | |
324 | + | |
325 | +.topmenu { | |
326 | + display: flex; | |
327 | + align-items: center; | |
328 | + padding: 12px 16px; | |
329 | + background: #fff; | |
330 | + cursor: pointer; | |
331 | + transition: background-color 0.2s; | |
332 | + border-bottom: 1px solid #e9ecef; | |
333 | +} | |
334 | + | |
335 | +.topmenu:hover { | |
336 | + background: #f8f9fa; | |
337 | +} | |
338 | + | |
339 | +.topmenu .arrow { | |
340 | + width: 16px; | |
341 | + height: 16px; | |
342 | + margin-right: 8px; | |
343 | + transition: transform 0.2s; | |
344 | +} | |
345 | + | |
346 | +.arrow-open { | |
347 | + transform: rotate(90deg); | |
348 | +} | |
349 | + | |
350 | +.topmenu img:not(.arrow) { | |
351 | + width: 20px; | |
352 | + height: 20px; | |
353 | + margin-right: 8px; | |
354 | +} | |
355 | + | |
356 | +.top-dept-name { | |
357 | + cursor: pointer; | |
358 | + padding: 2px 4px; | |
359 | + border-radius: 4px; | |
360 | + transition: background-color 0.2s; | |
361 | +} | |
362 | + | |
363 | +.top-dept-name:hover { | |
364 | + background: #e9ecef; | |
365 | +} | |
366 | + | |
367 | +.topmenu .top-dept-name.active-top { | |
368 | + background: #007bff; | |
369 | + color: white; | |
370 | +} | |
371 | + | |
372 | +.topmenu p { | |
373 | + flex: 1; | |
374 | + margin: 0; | |
375 | + font-weight: 500; | |
376 | + color: #333; | |
377 | +} | |
378 | + | |
379 | +.btn { | |
380 | + padding: 4px 8px; | |
381 | + border: none; | |
382 | + border-radius: 4px; | |
383 | + font-size: 12px; | |
384 | + cursor: pointer; | |
385 | + transition: all 0.2s; | |
386 | +} | |
387 | + | |
388 | +.btn.secondary { | |
389 | + background: #6c757d; | |
390 | + color: white; | |
391 | +} | |
392 | + | |
393 | +.btn.secondary:hover { | |
394 | + background: #5a6268; | |
395 | +} | |
396 | + | |
397 | +.btn.tertiary { | |
398 | + background: #f8f9fa; | |
399 | + color: #6c757d; | |
400 | + border: 1px solid #dee2e6; | |
401 | +} | |
402 | + | |
403 | +.btn.btn-red { | |
404 | + background: #dc3545; | |
405 | + color: white; | |
406 | +} | |
407 | + | |
408 | +.btn.btn-red:hover { | |
409 | + background: #c82333; | |
410 | +} | |
411 | + | |
412 | +.submenu-list { | |
413 | + list-style: none; | |
414 | + padding: 0; | |
415 | + margin: 0; | |
416 | + background: #f8f9fa; | |
417 | +} | |
418 | + | |
419 | +.submenu { | |
420 | + border-bottom: 1px solid #e9ecef; | |
421 | +} | |
422 | + | |
423 | +.submenu:last-child { | |
424 | + border-bottom: none; | |
425 | +} | |
426 | + | |
427 | +.submenu-item { | |
428 | + display: flex; | |
429 | + align-items: center; | |
430 | + padding: 10px 16px; | |
431 | + cursor: pointer; | |
432 | + transition: background-color 0.2s; | |
433 | +} | |
434 | + | |
435 | +.submenu-item:hover { | |
436 | + background: #e9ecef; | |
437 | +} | |
438 | + | |
439 | +.submenu-item.active-link { | |
440 | + background: #007bff; | |
441 | + color: white; | |
442 | +} | |
443 | + | |
444 | +.submenu-item img { | |
445 | + width: 16px; | |
446 | + height: 16px; | |
447 | + margin-right: 8px; | |
448 | +} | |
449 | + | |
450 | +.submenu-item p { | |
451 | + margin: 0; | |
452 | + font-size: 14px; | |
453 | +} | |
454 | + | |
455 | +.buttons { | |
456 | + margin-top: 16px; | |
457 | + text-align: center; | |
458 | +} | |
459 | + | |
460 | +.buttons button { | |
461 | + background: none; | |
462 | + border: none; | |
463 | + cursor: pointer; | |
464 | + padding: 8px; | |
465 | + border-radius: 4px; | |
466 | + transition: background-color 0.2s; | |
467 | +} | |
468 | + | |
469 | +.buttons button:hover { | |
470 | + background: #e9ecef; | |
471 | +} | |
472 | + | |
473 | +.title-wrap { | |
474 | + display: flex; | |
475 | + justify-content: space-between; | |
476 | + align-items: center; | |
477 | + margin-bottom: 16px; | |
478 | + padding: 16px; | |
479 | + background: #f8f9fa; | |
480 | + border-radius: 6px; | |
481 | + border: 1px solid #dee2e6; | |
482 | +} | |
483 | + | |
484 | +.title-wrap h3 { | |
485 | + display: flex; | |
486 | + align-items: center; | |
487 | + margin: 0; | |
488 | + font-size: 18px; | |
489 | + font-weight: 600; | |
490 | + color: #333; | |
491 | +} | |
492 | + | |
493 | +.title-wrap h3 img { | |
494 | + width: 20px; | |
495 | + height: 20px; | |
496 | + margin-right: 8px; | |
497 | +} | |
498 | + | |
499 | +.form-label { | |
500 | + font-weight: 500; | |
501 | + color: #333; | |
502 | + margin-bottom: 6px; | |
503 | +} | |
504 | + | |
505 | +.form-label p { | |
506 | + margin: 0; | |
507 | + display: flex; | |
508 | + align-items: center; | |
509 | +} | |
510 | + | |
511 | +.require { | |
512 | + margin-left: 4px; | |
513 | +} | |
514 | + | |
515 | +.require img { | |
516 | + width: 8px; | |
517 | + height: 8px; | |
518 | +} | |
519 | + | |
520 | +.form-control { | |
521 | + width: 100%; | |
522 | + padding: 8px 12px; | |
523 | + border: 1px solid #ced4da; | |
524 | + border-radius: 4px; | |
525 | + font-size: 14px; | |
526 | + transition: border-color 0.2s; | |
527 | +} | |
528 | + | |
529 | +.form-control:focus { | |
530 | + outline: none; | |
531 | + border-color: #007bff; | |
532 | + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); | |
533 | +} | |
534 | + | |
535 | +.form-control[readonly] { | |
536 | + background-color: #f8f9fa; | |
537 | + color: #6c757d; | |
538 | +} | |
539 | + | |
540 | +.textarea { | |
541 | + resize: vertical; | |
542 | + min-height: 80px; | |
543 | +} | |
544 | + | |
545 | +.tbl-wrap { | |
546 | + margin: 16px 0; | |
547 | + border: 1px solid #dee2e6; | |
548 | + border-radius: 6px; | |
549 | + overflow: hidden; | |
550 | +} | |
551 | + | |
552 | +.tbl { | |
553 | + width: 100%; | |
554 | + border-collapse: collapse; | |
555 | + background: white; | |
556 | +} | |
557 | + | |
558 | +.tbl thead th { | |
559 | + background: #f8f9fa; | |
560 | + padding: 12px; | |
561 | + text-align: center; | |
562 | + font-weight: 600; | |
563 | + color: #333; | |
564 | + border-bottom: 2px solid #dee2e6; | |
565 | +} | |
566 | + | |
567 | +.tbl tbody td { | |
568 | + padding: 12px; | |
569 | + text-align: center; | |
570 | + border-bottom: 1px solid #e9ecef; | |
571 | +} | |
572 | + | |
573 | +.tbl tbody tr:hover { | |
574 | + background: #f8f9fa; | |
575 | +} | |
576 | + | |
577 | +.form-check { | |
578 | + display: flex; | |
579 | + justify-content: center; | |
580 | + align-items: center; | |
581 | +} | |
582 | + | |
583 | +.form-check input[type="checkbox"], | |
584 | +.form-check input[type="radio"] { | |
585 | + margin: 0; | |
586 | + cursor: pointer; | |
587 | +} | |
588 | + | |
589 | +.pagination { | |
590 | + display: flex; | |
591 | + justify-content: center; | |
592 | + margin-top: 20px; | |
593 | +} | |
594 | + | |
595 | +.pagination ul { | |
596 | + display: flex; | |
597 | + list-style: none; | |
598 | + padding: 0; | |
599 | + margin: 0; | |
600 | + gap: 4px; | |
601 | +} | |
602 | + | |
603 | +.pagination li { | |
604 | + padding: 8px 12px; | |
605 | + border: 1px solid #dee2e6; | |
606 | + background: white; | |
607 | + cursor: pointer; | |
608 | + border-radius: 4px; | |
609 | + transition: all 0.2s; | |
610 | +} | |
611 | + | |
612 | +.pagination li:hover:not(.disabled) { | |
613 | + background: #f8f9fa; | |
614 | +} | |
615 | + | |
616 | +.pagination li.active { | |
617 | + background: #007bff; | |
618 | + color: white; | |
619 | + border-color: #007bff; | |
620 | +} | |
621 | + | |
622 | +.pagination li.disabled { | |
623 | + color: #6c757d; | |
624 | + cursor: not-allowed; | |
625 | + background: #f8f9fa; | |
626 | +} | |
627 | + | |
628 | +.col-12 { | |
629 | + margin-bottom: 16px; | |
630 | +} | |
631 | + | |
632 | +.row { | |
633 | + display: flex; | |
634 | + flex-direction: column; | |
635 | +} | |
636 | + | |
637 | +.pt-3 { | |
638 | + padding-top: 16px; | |
639 | +} | |
640 | + | |
641 | +.g-3 { | |
642 | + gap: 16px; | |
643 | +} | |
644 | +</style>(파일 끝에 줄바꿈 문자 없음) |
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?