--- client/views/pages/teacher/ExamDetail.vue
+++ client/views/pages/teacher/ExamDetail.vue
... | ... | @@ -2,61 +2,145 @@ |
2 | 2 |
<div class="title-box flex justify-between mb40"> |
3 | 3 |
<p class="title">문제 상세 페이지</p> |
4 | 4 |
</div> |
5 |
- <label for="" class="title1">문제 리스트</label> |
|
6 |
- <table class="mt20 mb100"> |
|
7 |
- <colgroup> |
|
8 |
- <col style="width: 10%;"> |
|
9 |
- <col style="width: 70%;"> |
|
10 |
- <col style="width: 20%;"> |
|
11 |
- </colgroup> |
|
12 |
- <thead> |
|
13 |
- <td>No.</td> |
|
14 |
- <td>문제</td> |
|
15 |
- <td>보기</td> |
|
16 |
- </thead> |
|
17 |
- <tbody> |
|
18 |
- <tr> |
|
19 |
- <td>1</td> |
|
20 |
- <td>1</td> |
|
21 |
- <td><button type="button" title="수정" class="new-btn"> |
|
22 |
- 수정 |
|
23 |
- </button></td> |
|
24 |
- </tr> |
|
25 |
- </tbody> |
|
26 |
- </table> |
|
27 |
- <label for="" class="title1">상세 내용</label> |
|
28 |
- <div class="board-wrap mt20"> |
|
29 |
- <div class="flex align-center mb20"> |
|
30 |
- <label for="" class="title2">문제</label> |
|
31 |
- <input type="text" class="data-wrap"> |
|
32 |
- </div> |
|
5 |
+ <div class="content-t"> |
|
6 |
+ <label for="" class="title1">상세 내용</label> |
|
7 |
+ <div class="board-wrap mt20"> |
|
8 |
+ <div class="flex align-center mb20"> |
|
9 |
+ <label for="" class="title2">단원</label> |
|
10 |
+ <select v-model="selectedUnit" class="mr10 data-wrap"> |
|
11 |
+ <option v-for="(unit, index) in units" :key="index" :value="unit.unitId"> |
|
12 |
+ {{ unit.unitName }} |
|
13 |
+ </option> |
|
14 |
+ </select> |
|
15 |
+ </div> |
|
16 |
+ <div class="flex align-center mb20"> |
|
17 |
+ <label for="" class="title2">평가 유형</label> |
|
18 |
+ <select v-model="selectedType" class="mr10 data-wrap"> |
|
19 |
+ <option value="중간평가">중간평가</option> |
|
20 |
+ <option value="최종평가">최종평가</option> |
|
21 |
+ </select> |
|
22 |
+ </div> |
|
33 | 23 |
|
34 |
- <hr> |
|
35 |
- <div class="flex align-center"> |
|
36 |
- <label for="" class="title2">내용</label> |
|
37 |
- <textarea name="" id="" class="data-wrap"></textarea> |
|
38 |
- </div> |
|
39 |
- <hr> |
|
40 |
- <div class="flex align-center mb20"> |
|
41 |
- <label for="" class="title2">첨부파일</label> |
|
42 |
- <input type="file" ref="fileInput" @change="handleFileUpload" /> |
|
43 |
- </div> |
|
44 |
- <div class="flex align-center"> |
|
45 |
- <label for="" class="title2">답</label> |
|
46 |
- <input type="text" class="data-wrap"> |
|
47 |
- </div> |
|
48 |
- </div> |
|
49 |
- <div class="flex justify-between mt50"> |
|
50 |
- <button type="button" title="글쓰기" class="new-btn" @click="goToPage('ExamList')"> |
|
51 |
- 목록 |
|
52 |
- </button> |
|
53 |
- <div class="flex"> |
|
54 |
- <button type="button" title="글쓰기" class="new-btn mr10"> |
|
55 |
- 수정 |
|
24 |
+ |
|
25 |
+ <hr> |
|
26 |
+ <button type="button" title="글쓰기" class="new-btn ml10" @click="buttonSearch"> |
|
27 |
+ 문제 추가 |
|
56 | 28 |
</button> |
57 |
- <button type="button" title="글쓰기" class="new-btn"> |
|
58 |
- 삭제 |
|
29 |
+ <hr> |
|
30 |
+ |
|
31 |
+ <div class="flex align-center mb20"> |
|
32 |
+ <table> |
|
33 |
+ <thead> |
|
34 |
+ <tr> |
|
35 |
+ <td>No.</td> |
|
36 |
+ <td>문제</td> |
|
37 |
+ <td>유형</td> |
|
38 |
+ <td>점수</td> |
|
39 |
+ <td>순서</td> |
|
40 |
+ <td></td> |
|
41 |
+ </tr> |
|
42 |
+ </thead> |
|
43 |
+ <tbody> |
|
44 |
+ <tr v-for="(evaluation, index) in evals" :key="evaluation.prblmId"> |
|
45 |
+ <td>{{ index + 1 }}</td> |
|
46 |
+ <td>{{ evaluation.prblmExpln }}</td> |
|
47 |
+ <td>{{ evaluation.prblmTypeNm }}</td> |
|
48 |
+ <td>{{ evaluation.prblmScr }}</td> |
|
49 |
+ <td><input v-model="evaluation.seq"></td> |
|
50 |
+ <td><button type="button" title="글쓰기" class="new-btn" |
|
51 |
+ @click="deleteEvalProblem(evaluation.prblmId)"> |
|
52 |
+ 삭제 |
|
53 |
+ </button></td> |
|
54 |
+ </tr> |
|
55 |
+ </tbody> |
|
56 |
+ </table> |
|
57 |
+ </div> |
|
58 |
+ </div> |
|
59 |
+ <div class="flex justify-between mt50"> |
|
60 |
+ <button type="button" title="글쓰기" class="new-btn" @click="goToPage('ExamList')"> |
|
61 |
+ 목록 |
|
59 | 62 |
</button> |
63 |
+ <div class="flex"> |
|
64 |
+ <button type="button" title="글쓰기" class="new-btn" @click="submitDetailForm"> |
|
65 |
+ 문제 수정 |
|
66 |
+ </button> |
|
67 |
+ <button type="button" title="글쓰기" class="new-btn" @click="deleteEval"> |
|
68 |
+ 평가 삭제 |
|
69 |
+ </button> |
|
70 |
+ </div> |
|
71 |
+ </div> |
|
72 |
+ <div v-show="searchOpen" class="popup-wrap"> |
|
73 |
+ <div class="popup-box "> |
|
74 |
+ <div class="flex justify-between mb30"> |
|
75 |
+ <p class="popup-title">문제 검색</p> |
|
76 |
+ <button type="button" class="popup-close-btn" @click="closeBtn"> |
|
77 |
+ <svg-icon type="mdi" :path="mdiWindowClose" class="close-btn"></svg-icon> |
|
78 |
+ |
|
79 |
+ </button> |
|
80 |
+ </div> |
|
81 |
+ <div class="search-wrap mb30"> |
|
82 |
+ <input type="text" class="data-wrap" placeholder="" v-model="searchKeyword"> |
|
83 |
+ <button type="button" @click="fetchProblems"> |
|
84 |
+ <img src="../../../resources/img/look_t.png" alt=""> |
|
85 |
+ </button> |
|
86 |
+ </div> |
|
87 |
+ <div class="table-wrap"> |
|
88 |
+ <table> |
|
89 |
+ <colgroup> |
|
90 |
+ <col style="width: 10%;"> |
|
91 |
+ <col style="width: 10%;"> |
|
92 |
+ <col style="width: 30%;"> |
|
93 |
+ <col style="width: 10%;"> |
|
94 |
+ <col style="width: 10%;"> |
|
95 |
+ <col style="width: 10%;"> |
|
96 |
+ <col style="width: 20%;"> |
|
97 |
+ </colgroup> |
|
98 |
+ <thead> |
|
99 |
+ <tr> |
|
100 |
+ <td></td> |
|
101 |
+ <td>No.</td> |
|
102 |
+ <td>문제</td> |
|
103 |
+ <td>유형</td> |
|
104 |
+ <td>점수</td> |
|
105 |
+ <td>작성자</td> |
|
106 |
+ <td>등록일</td> |
|
107 |
+ </tr> |
|
108 |
+ </thead> |
|
109 |
+ <tbody> |
|
110 |
+ <tr v-for="(problem, index) in problems" :key="problem.prblmId"> |
|
111 |
+ <td><input type="checkbox" v-model="problem.check"></td> |
|
112 |
+ <td>{{ index + 1 }}</td> |
|
113 |
+ <td>{{ problem.prblmExpln }}</td> |
|
114 |
+ <td>{{ problem.prblmTypeNm }}</td> |
|
115 |
+ <td>{{ problem.prblmScr }}</td> |
|
116 |
+ <td>{{ problem.userId }}</td> |
|
117 |
+ <td>{{ problem.regDt }}</td> |
|
118 |
+ </tr> |
|
119 |
+ </tbody> |
|
120 |
+ </table> |
|
121 |
+ <article class="table-pagination flex justify-center align-center mb20 mt30" style="gap: 10px;"> |
|
122 |
+ <button @click="changePage(currentPage - 1)" :disabled="currentPage === 1"> |
|
123 |
+ <img src="../../../resources/img/btn27_90t_normal.png" alt="Previous"> |
|
124 |
+ </button> |
|
125 |
+ <button v-for="page in paginationButtons" :key="page" @click="changePage(page)" |
|
126 |
+ :class="{ 'selected-btn': currentPage === page }"> |
|
127 |
+ {{ page }} |
|
128 |
+ </button> |
|
129 |
+ <button @click="changePage(currentPage + 1)" :disabled="currentPage === totalPages"> |
|
130 |
+ <img src="../../../resources/img/btn28_90t_normal.png" alt="Next"> |
|
131 |
+ </button> |
|
132 |
+ </article> |
|
133 |
+ |
|
134 |
+ </div> |
|
135 |
+ <div class="flex justify-end "> |
|
136 |
+ <button type="button" title="" class="new-btn mr10" @click="closeBtn"> |
|
137 |
+ 취소 |
|
138 |
+ </button> |
|
139 |
+ <button type="button" title="" class="new-btn" @click="insertEval"> |
|
140 |
+ 등록 |
|
141 |
+ </button> |
|
142 |
+ </div> |
|
143 |
+ </div> |
|
60 | 144 |
</div> |
61 | 145 |
</div> |
62 | 146 |
</template> |
... | ... | @@ -64,30 +148,289 @@ |
64 | 148 |
<script> |
65 | 149 |
import SvgIcon from '@jamescoyle/vue-icon'; |
66 | 150 |
import { mdiMagnify } from '@mdi/js'; |
151 |
+import axios from 'axios'; |
|
67 | 152 |
|
68 | 153 |
|
69 | 154 |
export default { |
70 | 155 |
data() { |
71 | 156 |
return { |
72 | 157 |
mdiMagnify: mdiMagnify, |
158 |
+ units: [], |
|
159 |
+ problems: [], |
|
160 |
+ selectedUnit: null, |
|
161 |
+ currentPage: 1, |
|
162 |
+ pageSize: 5, |
|
163 |
+ totalPosts: 0, |
|
164 |
+ searchOption: '', |
|
165 |
+ searchKeyword: '', |
|
166 |
+ searchOpen: false, |
|
167 |
+ evals: [], |
|
168 |
+ evalData: {}, |
|
73 | 169 |
} |
74 | 170 |
}, |
75 | 171 |
methods: { |
76 | 172 |
goToPage(page) { |
77 | 173 |
this.$router.push({ name: page }); |
78 | 174 |
}, |
175 |
+ buttonSearch() { |
|
176 |
+ this.searchOpen = true; |
|
177 |
+ this.fetchProblems(); |
|
178 |
+ }, |
|
179 |
+ closeBtn() { |
|
180 |
+ this.searchOpen = false; |
|
181 |
+ |
|
182 |
+ }, |
|
183 |
+ |
|
184 |
+ fetchUnits() { |
|
185 |
+ axios({ |
|
186 |
+ url: "/unit/findAll.json", |
|
187 |
+ method: "post", |
|
188 |
+ headers: { |
|
189 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
190 |
+ }, |
|
191 |
+ }) |
|
192 |
+ .then(response => { |
|
193 |
+ console.log(response.data) |
|
194 |
+ this.units = response.data; |
|
195 |
+ if (this.$route.query.unit_id) { |
|
196 |
+ this.selectedUnit = this.$route.query.unit_id; |
|
197 |
+ } |
|
198 |
+ }) |
|
199 |
+ .catch(error => { |
|
200 |
+ console.error("fetchUnits - error: ", error); |
|
201 |
+ alert("단원 목록을 불러오는 중 오류가 발생했습니다."); |
|
202 |
+ }); |
|
203 |
+ }, |
|
204 |
+ |
|
205 |
+ // 문제 가져오기 |
|
206 |
+ async fetchProblems(page = 1) { |
|
207 |
+ try { |
|
208 |
+ const response = await axios.post('/problem/problemList.json', { |
|
209 |
+ option: this.searchOption, |
|
210 |
+ keyword: this.searchKeyword, |
|
211 |
+ unitId: this.selectedUnit, |
|
212 |
+ pageSize: this.pageSize, |
|
213 |
+ startIndex: (page - 1) * 5 |
|
214 |
+ }); |
|
215 |
+ this.problems = response.data.problems; |
|
216 |
+ this.totalPosts = response.data.totalProblem; |
|
217 |
+ |
|
218 |
+ this.currentPage = page; |
|
219 |
+ } catch (error) { |
|
220 |
+ console.error('문제 목록을 불러오는 중 오류가 발생했습니다.', error); |
|
221 |
+ } |
|
222 |
+ }, |
|
223 |
+ changePage(page) { |
|
224 |
+ if (page < 1 || page > this.totalPages) return; |
|
225 |
+ this.currentPage = page; |
|
226 |
+ this.fetchProblems(page); |
|
227 |
+ }, |
|
228 |
+ |
|
229 |
+ // 팝업 데이터 가져오기 |
|
230 |
+ insertEval() { |
|
231 |
+ const selectedProblems = this.problems.filter(problem => problem.check).map(problem => ({ |
|
232 |
+ ...problem, |
|
233 |
+ isNew: true, // 새로운 문제라는 플래그 추가 |
|
234 |
+ })); |
|
235 |
+ this.evals.push(...selectedProblems); |
|
236 |
+ this.closeBtn(); |
|
237 |
+ }, |
|
238 |
+ |
|
239 |
+ |
|
240 |
+ // 평가 정보 가져오기 |
|
241 |
+ fetchEvalData() { |
|
242 |
+ axios({ |
|
243 |
+ url: "/evalProblem/selectEvalProblem.json", |
|
244 |
+ method: "post", |
|
245 |
+ headers: { |
|
246 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
247 |
+ }, |
|
248 |
+ data: { |
|
249 |
+ evalId: this.$route.query.eval_id |
|
250 |
+ }, |
|
251 |
+ }) |
|
252 |
+ .then(response => { |
|
253 |
+ this.evalData = response.data[0]; |
|
254 |
+ this.selectedType = this.evalData.eval_type; |
|
255 |
+ }) |
|
256 |
+ .catch(error => { |
|
257 |
+ console.error("fetchData - error: ", error); |
|
258 |
+ alert("검색 중 오류가 발생했습니다."); |
|
259 |
+ }); |
|
260 |
+ }, |
|
261 |
+ |
|
262 |
+ // 평가 문제 정보 가져오기 |
|
263 |
+ fetchEvalQues() { |
|
264 |
+ axios({ |
|
265 |
+ url: "/problem/evaluationProblemList.json", |
|
266 |
+ method: "post", |
|
267 |
+ headers: { |
|
268 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
269 |
+ }, |
|
270 |
+ data: { |
|
271 |
+ evalId: this.$route.query.eval_id |
|
272 |
+ }, |
|
273 |
+ }) |
|
274 |
+ .then(response => { |
|
275 |
+ this.evals = response.data.map(problem => ({ |
|
276 |
+ ...problem, |
|
277 |
+ originalSeq: problem.seq |
|
278 |
+ })); |
|
279 |
+ }) |
|
280 |
+ .catch(error => { |
|
281 |
+ console.error("fetchData - error: ", error); |
|
282 |
+ alert("검색 중 오류가 발생했습니다."); |
|
283 |
+ }); |
|
284 |
+ }, |
|
285 |
+ |
|
286 |
+ // 평가 문제 하나 삭제하기 |
|
287 |
+ deleteEvalProblem(prblmId) { |
|
288 |
+ const result = confirm('문제를 삭제 하시겠습니까?') |
|
289 |
+ if (result) { |
|
290 |
+ |
|
291 |
+ } else { |
|
292 |
+ alert("삭제를 취소했습니다") |
|
293 |
+ return; |
|
294 |
+ } |
|
295 |
+ |
|
296 |
+ const problem = this.evals.find(evaluation => evaluation.prblmId === prblmId); |
|
297 |
+ |
|
298 |
+ if (problem.isNew) { |
|
299 |
+ alert("새로 추가된 문제는 삭제할 수 없습니다."); |
|
300 |
+ return; |
|
301 |
+ } |
|
302 |
+ |
|
303 |
+ axios({ |
|
304 |
+ url: "/evalProblem/deleteEvalProblem.json", |
|
305 |
+ method: "post", |
|
306 |
+ headers: { |
|
307 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
308 |
+ }, |
|
309 |
+ data: { |
|
310 |
+ evalId: this.$route.query.eval_id, |
|
311 |
+ prblmId: prblmId, |
|
312 |
+ }, |
|
313 |
+ }) |
|
314 |
+ .then(response => { |
|
315 |
+ this.fetchEvalQues(); |
|
316 |
+ }) |
|
317 |
+ .catch(error => { |
|
318 |
+ console.error("fetchData - error: ", error); |
|
319 |
+ alert("검색 중 오류가 발생했습니다."); |
|
320 |
+ }); |
|
321 |
+ }, |
|
322 |
+ |
|
323 |
+ // 평가 문제 상세 업로드 |
|
324 |
+ async submitDetailForm() { |
|
325 |
+ const evalProblemVOList = this.evals |
|
326 |
+ .filter(evaluation => evaluation.isNew) |
|
327 |
+ .map(evaluation => ({ |
|
328 |
+ evalId: this.$route.query.eval_id, |
|
329 |
+ prblmId: evaluation.prblmId, |
|
330 |
+ })); |
|
331 |
+ |
|
332 |
+ const hasNewProblems = evalProblemVOList.length > 0; |
|
333 |
+ |
|
334 |
+ try { |
|
335 |
+ if (hasNewProblems) { |
|
336 |
+ const response = await axios.post('/evalProblem/insertEvalProblem.json', evalProblemVOList); |
|
337 |
+ console.log('성공:', response.data); |
|
338 |
+ } else { |
|
339 |
+ console.log('새로 추가된 문제가 없습니다.'); |
|
340 |
+ } |
|
341 |
+ |
|
342 |
+ // 순서가 변경된 문제가 있을 경우 updateSequence 실행 |
|
343 |
+ await this.updateSequence(); |
|
344 |
+ |
|
345 |
+ // 업데이트 후 평가 문제 목록 갱신 |
|
346 |
+ this.fetchEvalQues(); |
|
347 |
+ } catch (error) { |
|
348 |
+ console.error('오류:', error); |
|
349 |
+ } |
|
350 |
+ }, |
|
351 |
+ |
|
352 |
+ // 문제 정보 수정하기 (순서 변경) |
|
353 |
+ async updateSequence() { |
|
354 |
+ const evalList = this.evals |
|
355 |
+ .filter(evaluation => evaluation.seq !== evaluation.originalSeq) // 순서가 변경된 문제만 필터링 |
|
356 |
+ .map(evaluation => ({ |
|
357 |
+ prblmId: evaluation.prblmId, |
|
358 |
+ seq: evaluation.seq, |
|
359 |
+ })); |
|
360 |
+ |
|
361 |
+ if (evalList.length === 0) { |
|
362 |
+ console.log('순서가 변경된 문제가 없습니다.'); |
|
363 |
+ return; |
|
364 |
+ } |
|
365 |
+ |
|
366 |
+ try { |
|
367 |
+ const response = await axios.post('/problem/updateProblemSeq.json', evalList); |
|
368 |
+ console.log('순서 업데이트 성공:', response.data); |
|
369 |
+ } catch (error) { |
|
370 |
+ console.error('순서 업데이트 오류:', error); |
|
371 |
+ } |
|
372 |
+ }, |
|
373 |
+ |
|
374 |
+ // 평가 자체를 삭제하기 |
|
375 |
+ deleteEval() { |
|
376 |
+ const result = confirm('이 평가를 삭제 하시겠습니까?') |
|
377 |
+ if (result) { |
|
378 |
+ |
|
379 |
+ } else { |
|
380 |
+ alert("삭제를 취소했습니다") |
|
381 |
+ return; |
|
382 |
+ } |
|
383 |
+ |
|
384 |
+ axios({ |
|
385 |
+ url: "/evaluation/deleteEvaluation.json", |
|
386 |
+ method: "post", |
|
387 |
+ headers: { |
|
388 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
389 |
+ }, |
|
390 |
+ data: { |
|
391 |
+ evalId: this.$route.query.eval_id, |
|
392 |
+ }, |
|
393 |
+ }) |
|
394 |
+ .then(response => { |
|
395 |
+ this.goToPage('ExamList'); |
|
396 |
+ }) |
|
397 |
+ .catch(error => { |
|
398 |
+ console.error("fetchData - error: ", error); |
|
399 |
+ alert("삭제 중 오류가 발생했습니다."); |
|
400 |
+ }); |
|
401 |
+ }, |
|
79 | 402 |
}, |
80 | 403 |
watch: { |
81 | 404 |
|
82 | 405 |
}, |
83 | 406 |
computed: { |
407 |
+ totalPages() { |
|
408 |
+ return Math.ceil(this.totalPosts / this.pageSize); |
|
409 |
+ }, |
|
410 |
+ paginationButtons() { |
|
411 |
+ let start = Math.max(0, this.currentPage - 2); |
|
412 |
+ let end = Math.min(start + 5, this.totalPages); |
|
413 |
+ |
|
414 |
+ if (end - start < 5) { |
|
415 |
+ start = Math.max(0, end - 5); |
|
416 |
+ } |
|
417 |
+ |
|
418 |
+ return Array.from({ length: end - start }, (_, i) => start + i + 1); |
|
419 |
+ }, |
|
420 |
+ |
|
421 |
+ startIndex() { |
|
422 |
+ return this.currentPage * this.itemsPerPage; |
|
423 |
+ } |
|
424 |
+ |
|
84 | 425 |
|
85 | 426 |
}, |
86 | 427 |
components: { |
87 | 428 |
SvgIcon |
88 | 429 |
}, |
89 | 430 |
mounted() { |
90 |
- console.log('Main2 mounted'); |
|
431 |
+ this.fetchUnits(); |
|
432 |
+ this.fetchEvalData(); |
|
433 |
+ this.fetchEvalQues(); |
|
91 | 434 |
} |
92 | 435 |
} |
93 | 436 |
</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/teacher/ExamInsert.vue
+++ client/views/pages/teacher/ExamInsert.vue
... | ... | @@ -24,101 +24,312 @@ |
24 | 24 |
</tr> |
25 | 25 |
</tbody> |
26 | 26 |
</table> --> |
27 |
+ <div class="content-t"> |
|
28 |
+ |
|
27 | 29 |
<label for="" class="title1">상세 내용</label> |
28 | 30 |
<div class="board-wrap mt20"> |
29 | 31 |
<div class="flex align-center mb20"> |
30 | 32 |
<label for="" class="title2">단원</label> |
31 |
- <select v-model="selectedSearchOption" class="mr10 data-wrap"> |
|
32 |
- <option value="bbsTtl">제목</option> |
|
33 |
- <option value="bbsCnt">내용</option> |
|
34 |
- <option value="userNm">작성자</option> |
|
35 |
- <option value="bbsCls">카테고리</option> |
|
33 |
+ <select v-model="selectedUnit" class="mr10 data-wrap"> |
|
34 |
+ <option v-for="(unit, index) in units" :key="index" :value="unit.unitId"> |
|
35 |
+ {{ unit.unitName }} |
|
36 |
+ </option> |
|
36 | 37 |
</select> |
37 | 38 |
</div> |
38 | 39 |
<div class="flex align-center mb20"> |
39 | 40 |
<label for="" class="title2">평가 유형</label> |
40 |
- <select v-model="selectedSearchOption" class="mr10 data-wrap"> |
|
41 |
- <option value="bbsTtl">제목</option> |
|
42 |
- <option value="bbsCnt">내용</option> |
|
43 |
- <option value="userNm">작성자</option> |
|
44 |
- <option value="bbsCls">카테고리</option> |
|
41 |
+ <select v-model="selectedType" class="mr10 data-wrap"> |
|
42 |
+ <option value="중간평가">중간평가</option> |
|
43 |
+ <option value="최종평가">최종평가</option> |
|
45 | 44 |
</select> |
46 | 45 |
</div> |
47 | 46 |
|
48 | 47 |
<hr> |
48 |
+ <button type="button" title="글쓰기" class="new-btn ml10" @click="buttonSearch"> |
|
49 |
+ 문제 추가 |
|
50 |
+ </button> |
|
51 |
+ <hr> |
|
52 |
+ |
|
49 | 53 |
<div class="flex align-center mb20"> |
50 |
- <label for="" class="title2">문제 1</label> |
|
51 |
- <select v-model="selectedSearchOption" class="mr10 data-wrap"> |
|
52 |
- <option value="bbsTtl">제목</option> |
|
53 |
- <option value="bbsCnt">내용</option> |
|
54 |
- <option value="userNm">작성자</option> |
|
55 |
- <option value="bbsCls">카테고리</option> |
|
56 |
- </select> |
|
54 |
+ <table> |
|
55 |
+ <thead> |
|
56 |
+ <tr> |
|
57 |
+ <td>No.</td> |
|
58 |
+ <td>문제</td> |
|
59 |
+ <td>유형</td> |
|
60 |
+ <td>점수</td> |
|
61 |
+ <td>순서</td> |
|
62 |
+ </tr> |
|
63 |
+ </thead> |
|
64 |
+ <tbody> |
|
65 |
+ <tr v-for="(evaluation, index) in evals" :key="evaluation.prblmId"> |
|
66 |
+ <td>{{ index + 1 }}</td> |
|
67 |
+ <td>{{ evaluation.prblmExpln }}</td> |
|
68 |
+ <td>{{ evaluation.prblmTypeNm }}</td> |
|
69 |
+ <td>{{ evaluation.prblmScr }}</td> |
|
70 |
+ <td><input v-model="evaluation.seq"></td> |
|
71 |
+ </tr> |
|
72 |
+ </tbody> |
|
73 |
+ </table> |
|
57 | 74 |
</div> |
58 |
- <div class="flex align-center mb20"> |
|
59 |
- <label for="" class="title2">문제 2</label> |
|
60 |
- <select v-model="selectedSearchOption" class="mr10 data-wrap"> |
|
61 |
- <option value="bbsTtl">제목</option> |
|
62 |
- <option value="bbsCnt">내용</option> |
|
63 |
- <option value="userNm">작성자</option> |
|
64 |
- <option value="bbsCls">카테고리</option> |
|
65 |
- </select> |
|
66 |
- </div> |
|
67 |
- <div class="flex align-center mb20"> |
|
68 |
- <label for="" class="title2">문제 3</label> |
|
69 |
- <select v-model="selectedSearchOption" class="mr10 data-wrap"> |
|
70 |
- <option value="bbsTtl">제목</option> |
|
71 |
- <option value="bbsCnt">내용</option> |
|
72 |
- <option value="userNm">작성자</option> |
|
73 |
- <option value="bbsCls">카테고리</option> |
|
74 |
- </select> |
|
75 |
- </div> |
|
76 |
- <div class="flex align-center mb20"> |
|
77 |
- <label for="" class="title2">문제 4</label> |
|
78 |
- <select v-model="selectedSearchOption" class="mr10 data-wrap"> |
|
79 |
- <option value="bbsTtl">제목</option> |
|
80 |
- <option value="bbsCnt">내용</option> |
|
81 |
- <option value="userNm">작성자</option> |
|
82 |
- <option value="bbsCls">카테고리</option> |
|
83 |
- </select> |
|
84 |
- </div> |
|
85 |
- |
|
86 | 75 |
</div> |
87 | 76 |
<div class="flex justify-between mt50"> |
88 |
- <button type="button" title="글쓰기" class="new-btn" @click="goToPage('C_ExamList')"> |
|
77 |
+ <button type="button" title="글쓰기" class="new-btn" @click="goToPage('ExamList')"> |
|
89 | 78 |
목록 |
90 | 79 |
</button> |
91 | 80 |
<div class="flex"> |
92 |
- <button type="button" title="글쓰기" class="new-btn mr10"> |
|
93 |
- 취소 |
|
94 |
- </button> |
|
95 |
- <button type="button" title="글쓰기" class="new-btn"> |
|
81 |
+ <button type="button" title="글쓰기" class="new-btn" @click="submitForm"> |
|
96 | 82 |
등록 |
97 | 83 |
</button> |
98 | 84 |
</div> |
85 |
+ </div> |
|
86 |
+ <div v-show="searchOpen" class="popup-wrap"> |
|
87 |
+ <div class="popup-box "> |
|
88 |
+ <div class="flex justify-between mb30"> |
|
89 |
+ <p class="popup-title">문제 검색</p> |
|
90 |
+ <button type="button" class="popup-close-btn" @click="closeBtn"> |
|
91 |
+ <svg-icon type="mdi" :path="mdiWindowClose" class="close-btn"></svg-icon> |
|
92 |
+ |
|
93 |
+ </button> |
|
94 |
+ </div> |
|
95 |
+ <div class="search-wrap mb30"> |
|
96 |
+ <input type="text" class="data-wrap" placeholder="" v-model="searchKeyword"> |
|
97 |
+ <button type="button" @click="fetchProblems"> |
|
98 |
+ <img src="../../../resources/img/look_t.png" alt=""> |
|
99 |
+ </button> |
|
100 |
+ </div> |
|
101 |
+ <div class="table-wrap"> |
|
102 |
+ <table> |
|
103 |
+ <colgroup> |
|
104 |
+ <col style="width: 10%;"> |
|
105 |
+ <col style="width: 10%;"> |
|
106 |
+ <col style="width: 30%;"> |
|
107 |
+ <col style="width: 10%;"> |
|
108 |
+ <col style="width: 10%;"> |
|
109 |
+ <col style="width: 10%;"> |
|
110 |
+ <col style="width: 20%;"> |
|
111 |
+ </colgroup> |
|
112 |
+ <thead> |
|
113 |
+ <tr> |
|
114 |
+ <td></td> |
|
115 |
+ <td>No.</td> |
|
116 |
+ <td>문제</td> |
|
117 |
+ <td>유형</td> |
|
118 |
+ <td>점수</td> |
|
119 |
+ <td>작성자</td> |
|
120 |
+ <td>등록일</td> |
|
121 |
+ </tr> |
|
122 |
+ </thead> |
|
123 |
+ <tbody> |
|
124 |
+ <tr v-for="(problem, index) in problems" :key="problem.prblmId"> |
|
125 |
+ <td><input type="checkbox" v-model="problem.check"></td> |
|
126 |
+ <td>{{ index + 1 }}</td> |
|
127 |
+ <td>{{ problem.prblmExpln }}</td> |
|
128 |
+ <td>{{ problem.prblmTypeNm }}</td> |
|
129 |
+ <td>{{ problem.prblmScr }}</td> |
|
130 |
+ <td>{{ problem.userId }}</td> |
|
131 |
+ <td>{{ problem.regDt }}</td> |
|
132 |
+ </tr> |
|
133 |
+ </tbody> |
|
134 |
+ </table> |
|
135 |
+ <article class="table-pagination flex justify-center align-center mb20 mt30" style="gap: 10px;"> |
|
136 |
+ <button @click="changePage(currentPage - 1)" :disabled="currentPage === 1"> |
|
137 |
+ <img src="../../../resources/img/btn27_90t_normal.png" alt="Previous"> |
|
138 |
+ </button> |
|
139 |
+ <button v-for="page in paginationButtons" :key="page" @click="changePage(page)" |
|
140 |
+ :class="{ 'selected-btn': currentPage === page }"> |
|
141 |
+ {{ page }} |
|
142 |
+ </button> |
|
143 |
+ <button @click="changePage(currentPage + 1)" :disabled="currentPage === totalPages"> |
|
144 |
+ <img src="../../../resources/img/btn28_90t_normal.png" alt="Next"> |
|
145 |
+ </button> |
|
146 |
+ </article> |
|
147 |
+ |
|
148 |
+ </div> |
|
149 |
+ <div class="flex justify-end "> |
|
150 |
+ <button type="button" title="" class="new-btn mr10" @click="closeBtn"> |
|
151 |
+ 취소 |
|
152 |
+ </button> |
|
153 |
+ <button type="button" title="" class="new-btn" @click="insertEval"> |
|
154 |
+ 등록 |
|
155 |
+ </button> |
|
156 |
+ </div> |
|
157 |
+ </div> |
|
158 |
+ </div> |
|
99 | 159 |
</div> |
100 | 160 |
</template> |
101 | 161 |
|
102 | 162 |
<script> |
103 | 163 |
import SvgIcon from '@jamescoyle/vue-icon'; |
104 | 164 |
import { mdiMagnify } from '@mdi/js'; |
105 |
- |
|
165 |
+import axios from 'axios'; |
|
106 | 166 |
|
107 | 167 |
export default { |
108 | 168 |
data() { |
109 | 169 |
return { |
110 | 170 |
mdiMagnify: mdiMagnify, |
171 |
+ units: [], |
|
172 |
+ problems: [], |
|
173 |
+ selectedUnit: null, |
|
174 |
+ currentPage: 1, |
|
175 |
+ pageSize: 5, |
|
176 |
+ totalPosts: 0, |
|
177 |
+ searchOption: '', |
|
178 |
+ searchKeyword: '', |
|
179 |
+ searchOpen: false, |
|
180 |
+ evals: [], |
|
111 | 181 |
} |
112 | 182 |
}, |
113 | 183 |
methods: { |
114 | 184 |
goToPage(page) { |
115 |
- this.$router.push({ name: page }); |
|
185 |
+ this.$router.push({ name: page, query: { unit_id: this.selectedUnit } }); |
|
186 |
+ }, |
|
187 |
+ |
|
188 |
+ buttonSearch() { |
|
189 |
+ this.searchOpen = true; |
|
190 |
+ this.fetchProblems(); |
|
191 |
+ }, |
|
192 |
+ closeBtn() { |
|
193 |
+ this.searchOpen = false; |
|
194 |
+ |
|
195 |
+ }, |
|
196 |
+ |
|
197 |
+ fetchUnits() { |
|
198 |
+ axios({ |
|
199 |
+ url: "/unit/findAll.json", |
|
200 |
+ method: "post", |
|
201 |
+ headers: { |
|
202 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
203 |
+ }, |
|
204 |
+ }) |
|
205 |
+ .then(response => { |
|
206 |
+ console.log(response.data) |
|
207 |
+ this.units = response.data; |
|
208 |
+ if (this.$route.query.unit_id) { |
|
209 |
+ this.selectedUnit = this.$route.query.unit_id; |
|
210 |
+ } |
|
211 |
+ }) |
|
212 |
+ .catch(error => { |
|
213 |
+ console.error("fetchUnits - error: ", error); |
|
214 |
+ alert("단원 목록을 불러오는 중 오류가 발생했습니다."); |
|
215 |
+ }); |
|
216 |
+ }, |
|
217 |
+ |
|
218 |
+ // 문제 가져오기 |
|
219 |
+ async fetchProblems(page = 1) { |
|
220 |
+ try { |
|
221 |
+ const response = await axios.post('/problem/problemList.json', { |
|
222 |
+ option: this.searchOption, |
|
223 |
+ keyword: this.searchKeyword, |
|
224 |
+ unitId: this.selectedUnit, |
|
225 |
+ pageSize: this.pageSize, |
|
226 |
+ startIndex: (page - 1) * 5 |
|
227 |
+ }); |
|
228 |
+ this.problems = response.data.problems; |
|
229 |
+ this.totalPosts = response.data.totalProblem; |
|
230 |
+ |
|
231 |
+ this.currentPage = page; |
|
232 |
+ } catch (error) { |
|
233 |
+ console.error('문제 목록을 불러오는 중 오류가 발생했습니다.', error); |
|
234 |
+ } |
|
235 |
+ }, |
|
236 |
+ changePage(page) { |
|
237 |
+ if (page < 1 || page > this.totalPages) return; |
|
238 |
+ this.currentPage = page; |
|
239 |
+ this.fetchProblems(page); |
|
240 |
+ }, |
|
241 |
+ |
|
242 |
+ // 팝업 데이터 가져오기 |
|
243 |
+ insertEval() { |
|
244 |
+ const selectedProblems = this.problems.filter(problem => problem.check); |
|
245 |
+ this.evals.push(...selectedProblems); |
|
246 |
+ this.closeBtn(); |
|
247 |
+ }, |
|
248 |
+ |
|
249 |
+ // 평가 데이터 등록하기 |
|
250 |
+ async submitForm() { |
|
251 |
+ // 필요한 모든 필드를 검사합니다. |
|
252 |
+ if (!this.selectedUnit) { |
|
253 |
+ alert("단원을 지정해 주세요."); |
|
254 |
+ return; |
|
255 |
+ } |
|
256 |
+ |
|
257 |
+ if (!this.selectedType) { |
|
258 |
+ alert("평가 유형을 지정해 주세요."); |
|
259 |
+ return; |
|
260 |
+ } |
|
261 |
+ |
|
262 |
+ const payload = { |
|
263 |
+ evalType: this.selectedType, |
|
264 |
+ unitId: this.selectedUnit |
|
265 |
+ }; |
|
266 |
+ |
|
267 |
+ try { |
|
268 |
+ const response = await axios.post('/evaluation/insertEvaluation.json', payload); |
|
269 |
+ console.log('성공:', response.data); |
|
270 |
+ const evalId = response.data.evalId; |
|
271 |
+ await this.submitDetailForm(evalId); |
|
272 |
+ await this.updateSequence(); |
|
273 |
+ |
|
274 |
+ this.goToPage('ExamList'); |
|
275 |
+ |
|
276 |
+ } catch (error) { |
|
277 |
+ console.error('오류:', error); |
|
278 |
+ } |
|
279 |
+ }, |
|
280 |
+ |
|
281 |
+ // 평가 문제 상세 업로드 |
|
282 |
+ async submitDetailForm(evalId) { |
|
283 |
+ const evalProblemVOList = this.evals.map(evaluation => ({ |
|
284 |
+ evalId: evalId, |
|
285 |
+ prblmId: evaluation.prblmId, |
|
286 |
+ })); |
|
287 |
+ |
|
288 |
+ try { |
|
289 |
+ const response = await axios.post('/evalProblem/insertEvalProblem.json', evalProblemVOList); |
|
290 |
+ console.log('성공:', response.data); |
|
291 |
+ } catch (error) { |
|
292 |
+ console.error('오류:', error); |
|
293 |
+ } |
|
294 |
+ }, |
|
295 |
+ |
|
296 |
+ // 문제 정보 수정하기 (순서 변경) |
|
297 |
+ async updateSequence(){ |
|
298 |
+ const evalList = this.evals.map(evaluation => ({ |
|
299 |
+ prblmId: evaluation.prblmId, |
|
300 |
+ seq: evaluation.seq, |
|
301 |
+ })); |
|
302 |
+ |
|
303 |
+ try { |
|
304 |
+ const response = await axios.post('/problem/updateProblemSeq.json', evalList); |
|
305 |
+ console.log('성공:', response.data); |
|
306 |
+ } catch (error) { |
|
307 |
+ console.error('오류:', error); |
|
308 |
+ } |
|
116 | 309 |
}, |
117 | 310 |
}, |
118 | 311 |
watch: { |
119 | 312 |
|
120 | 313 |
}, |
121 | 314 |
computed: { |
315 |
+ totalPages() { |
|
316 |
+ return Math.ceil(this.totalPosts / this.pageSize); |
|
317 |
+ }, |
|
318 |
+ paginationButtons() { |
|
319 |
+ let start = Math.max(0, this.currentPage - 2); |
|
320 |
+ let end = Math.min(start + 5, this.totalPages); |
|
321 |
+ |
|
322 |
+ if (end - start < 5) { |
|
323 |
+ start = Math.max(0, end - 5); |
|
324 |
+ } |
|
325 |
+ |
|
326 |
+ return Array.from({ length: end - start }, (_, i) => start + i + 1); |
|
327 |
+ }, |
|
328 |
+ |
|
329 |
+ startIndex() { |
|
330 |
+ return this.currentPage * this.itemsPerPage; |
|
331 |
+ } |
|
332 |
+ |
|
122 | 333 |
|
123 | 334 |
}, |
124 | 335 |
components: { |
... | ... | @@ -126,6 +337,8 @@ |
126 | 337 |
}, |
127 | 338 |
mounted() { |
128 | 339 |
console.log('Main2 mounted'); |
340 |
+ this.fetchUnits(); |
|
341 |
+ |
|
129 | 342 |
} |
130 | 343 |
} |
131 | 344 |
</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/teacher/ExamList.vue
+++ client/views/pages/teacher/ExamList.vue
... | ... | @@ -1,51 +1,49 @@ |
1 | 1 |
<template> |
2 | 2 |
<div class="title-box flex justify-between mb40"> |
3 | 3 |
<p class="title">평가</p> |
4 |
- <select name="" id=""> |
|
5 |
- <option value="">1단원</option> |
|
4 |
+ <select name="" id="" v-model="selectedBook" @change="fetchUnits"> |
|
5 |
+ <option value="" disabled selected>교재를 선택하세요</option> |
|
6 |
+ <option v-for="book in books" :key="book.book_id" :value="book.book_id"> |
|
7 |
+ {{ book.book_nm }} |
|
8 |
+ </option> |
|
6 | 9 |
</select> |
7 | 10 |
</div> |
8 |
- <label for="" class="title2">단원</label> |
|
11 |
+ <div class="content-t"> |
|
12 |
+ |
|
13 |
+ <label for="" class="title2">단원</label> |
|
9 | 14 |
<div class="unit-pagination flex mt10 mb20" style="gap: 10px;"> |
10 |
- <button class="selected-btn">1</button> |
|
11 |
- <button>2</button> |
|
12 |
- <button>3</button> |
|
15 |
+ <button v-for="(unit, index) in units" :key="index" |
|
16 |
+ :class="{ 'selected-btn': selectedUnit === unit.unitId }" @click="selectUnit(unit.unitId)"> |
|
17 |
+ {{ unit.unitName }} |
|
18 |
+ </button> |
|
13 | 19 |
</div> |
14 |
- <div class="search-wrap flex justify-end mb20"> |
|
15 |
- <select name="" id="" class="mr10 data-wrap"> |
|
16 |
- <option value="">중간</option> |
|
17 |
- <option value="">최종</option> |
|
18 |
- </select> |
|
19 |
- <select name="" id="" class="mr10 data-wrap"> |
|
20 |
+ <div class="search-wrap flex justify-end mb20"> |
|
21 |
+ <select id="evalType" class="mr10 data-wrap"> |
|
20 | 22 |
<option value="">전체</option> |
23 |
+ <option value="중간평가">중간</option> |
|
24 |
+ <option value="최종평가">최종</option> |
|
21 | 25 |
</select> |
22 |
- <input type="text" placeholder="검색하세요."> |
|
23 |
- <button type="button" title="위원회 검색"> |
|
24 |
- <img src="../../../resources/img/look_t.png" alt=""> |
|
25 |
- </button> |
|
26 |
+ <button type="button" title="위원회 검색" @click="fetchData"> |
|
27 |
+ <img src="../../../resources/img/look_t.png" alt=""> |
|
28 |
+ </button> |
|
26 | 29 |
</div> |
27 | 30 |
<div class="table-wrap"> |
28 | 31 |
<table> |
29 | 32 |
<thead> |
30 | 33 |
<td>No.</td> |
31 |
- <td>제목</td> |
|
32 | 34 |
<td>중간/최종</td> |
33 |
- <td>작성자</td> |
|
34 |
- <td>문항</td> |
|
35 |
+ <td>문항 갯수</td> |
|
35 | 36 |
<td>보기</td> |
36 |
- <td>등록일</td> |
|
37 | 37 |
</thead> |
38 | 38 |
<tbody> |
39 |
- <tr> |
|
40 |
- <td></td> |
|
41 |
- <td></td> |
|
42 |
- <td></td> |
|
43 |
- <td></td> |
|
44 |
- <td></td> |
|
45 |
- <td><button type="button" title="보기" class="new-btn" @click="goToPage('ExamDetail')"> |
|
46 |
- 보기 |
|
47 |
- </button></td> |
|
48 |
- <td></td> |
|
39 |
+ <tr v-for="(post, index) in posts" :key="post.evalId"> |
|
40 |
+ <td>{{ index + 1 }}</td> |
|
41 |
+ <td>{{ post.evalType }}</td> |
|
42 |
+ <td>{{ post.problemCount }}</td> |
|
43 |
+ <td><button type="button" title="보기" class="new-btn" |
|
44 |
+ @click="goToPage('ExamDetail', post.evalId)"> |
|
45 |
+ 보기 |
|
46 |
+ </button></td> |
|
49 | 47 |
</tr> |
50 | 48 |
<!-- <tr :class="{ 'hidden-tr': !isRowVisible }" class="show-tr"> |
51 | 49 |
<td colspan="7"> |
... | ... | @@ -76,44 +74,49 @@ |
76 | 74 |
</tr> --> |
77 | 75 |
</tbody> |
78 | 76 |
</table> |
79 |
- <article class="table-pagination flex justify-center align-center mb20 mt30" style="gap: 10px;"> |
|
80 |
- <button><img src="../../../resources/img/btn27_90t_normal.png" alt=""></button> |
|
81 |
- <button class="selected-btn">1</button> |
|
82 |
- <button>2</button> |
|
83 |
- <button>3</button> |
|
84 |
- <button><img src="../../../resources/img/btn28_90t_normal.png" alt=""></button> |
|
85 |
- </article> |
|
86 |
- <div class="flex justify-end "> |
|
77 |
+ <div class="flex justify-end "> |
|
87 | 78 |
<button type="button" title="등록" class="new-btn" @click="goToPage('ExamInsert')"> |
88 | 79 |
등록 |
89 | 80 |
</button> |
81 |
+ </div> |
|
90 | 82 |
</div> |
91 |
- </div> |
|
83 |
+ </div> |
|
92 | 84 |
</template> |
93 | 85 |
|
94 | 86 |
<script> |
95 | 87 |
import SvgIcon from '@jamescoyle/vue-icon'; |
96 |
-import { mdiMagnify, mdiWindowClose} from '@mdi/js'; |
|
88 |
+import { mdiMagnify, mdiWindowClose } from '@mdi/js'; |
|
89 |
+import axios from 'axios'; |
|
97 | 90 |
|
98 | 91 |
|
99 | 92 |
export default { |
100 |
- data () { |
|
93 |
+ data() { |
|
101 | 94 |
return { |
102 | 95 |
mdiMagnify: mdiMagnify, |
103 | 96 |
mdiWindowClose: mdiWindowClose, |
104 | 97 |
showModal: false, |
105 | 98 |
searchOpen: false, |
106 |
- isRowVisible: false |
|
99 |
+ isRowVisible: false, |
|
100 |
+ books: [], |
|
101 |
+ units: [], |
|
102 |
+ selectedBook: "", |
|
103 |
+ currentPage: 1, |
|
104 |
+ pageSize: 10, |
|
105 |
+ totalPosts: 0, |
|
106 |
+ posts: [], |
|
107 |
+ selectedUnit: null, |
|
108 |
+ selectedEval: null, |
|
109 |
+ |
|
107 | 110 |
} |
108 | 111 |
}, |
109 | 112 |
methods: { |
110 | 113 |
toggleRow() { |
111 |
- this.isRowVisible = !this.isRowVisible; |
|
112 |
- }, |
|
113 |
- goToPage(page) { |
|
114 |
- this.$router.push({ name: page }); |
|
115 |
- }, |
|
116 |
- showConfirm(type) { |
|
114 |
+ this.isRowVisible = !this.isRowVisible; |
|
115 |
+ }, |
|
116 |
+ goToPage(page, evalId) { |
|
117 |
+ this.$router.push({ name: page, query: { unit_id: this.selectedUnit, eval_id: evalId } }); |
|
118 |
+ }, |
|
119 |
+ showConfirm(type) { |
|
117 | 120 |
let message = ''; |
118 | 121 |
if (type === 'cancel') { |
119 | 122 |
message = '삭제하시겠습니까?'; |
... | ... | @@ -127,7 +130,11 @@ |
127 | 130 |
this.goBack(); |
128 | 131 |
} |
129 | 132 |
}, |
130 |
- |
|
133 |
+ selectUnit(unitId) { |
|
134 |
+ this.selectedUnit = unitId; |
|
135 |
+ this.fetchData(); |
|
136 |
+ }, |
|
137 |
+ |
|
131 | 138 |
closeModal() { |
132 | 139 |
this.showModal = false; |
133 | 140 |
}, |
... | ... | @@ -139,18 +146,106 @@ |
139 | 146 |
|
140 | 147 |
}, |
141 | 148 |
|
149 |
+ // 평가 정보 가져오기 |
|
150 |
+ fetchData() { |
|
151 |
+ const evalType = document.getElementById('evalType').value; |
|
152 |
+ const keyword = ''; |
|
153 |
+ const idx = (this.currentPage - 1) * this.pageSize; |
|
154 |
+ |
|
155 |
+ let option = null; |
|
156 |
+ let searchKeyword = null; |
|
157 |
+ |
|
158 |
+ if (evalType !== '') { |
|
159 |
+ option = 'eval_type'; |
|
160 |
+ searchKeyword = evalType; |
|
161 |
+ } else if (keyword !== '') { |
|
162 |
+ option = 'keyword'; |
|
163 |
+ searchKeyword = keyword; |
|
164 |
+ } |
|
165 |
+ axios({ |
|
166 |
+ url: "/evaluation/evaluationUnitList.json", |
|
167 |
+ method: "post", |
|
168 |
+ headers: { |
|
169 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
170 |
+ }, |
|
171 |
+ data: { |
|
172 |
+ "option": option, |
|
173 |
+ "keyword": searchKeyword, |
|
174 |
+ "pageSize": this.pageSize, |
|
175 |
+ "startIndex": idx, |
|
176 |
+ "unitId": this.selectedUnit |
|
177 |
+ }, |
|
178 |
+ }) |
|
179 |
+ .then(response => { |
|
180 |
+ this.posts = response.data; |
|
181 |
+ }) |
|
182 |
+ .catch(error => { |
|
183 |
+ console.error("fetchData - error: ", error); |
|
184 |
+ alert("검색 중 오류가 발생했습니다."); |
|
185 |
+ }); |
|
186 |
+ }, |
|
187 |
+ |
|
188 |
+ fetchBooks() { |
|
189 |
+ axios({ |
|
190 |
+ url: "/book/findAll.json", |
|
191 |
+ method: "post", |
|
192 |
+ headers: { |
|
193 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
194 |
+ }, |
|
195 |
+ }) |
|
196 |
+ .then(response => { |
|
197 |
+ console.log(response.data) |
|
198 |
+ this.books = response.data; |
|
199 |
+ |
|
200 |
+ }) |
|
201 |
+ .catch(error => { |
|
202 |
+ console.error("fetchBooks - error: ", error); |
|
203 |
+ alert("교재 목록을 불러오는 중 오류가 발생했습니다."); |
|
204 |
+ }); |
|
205 |
+ }, |
|
206 |
+ fetchUnits() { |
|
207 |
+ if (!this.selectedBook) return; |
|
208 |
+ |
|
209 |
+ axios({ |
|
210 |
+ url: "/unit/unitList.json", |
|
211 |
+ method: "post", |
|
212 |
+ headers: { |
|
213 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
214 |
+ }, |
|
215 |
+ data: { |
|
216 |
+ "bookId": this.selectedBook |
|
217 |
+ }, |
|
218 |
+ }) |
|
219 |
+ .then(response => { |
|
220 |
+ console.log(response.data) |
|
221 |
+ this.units = response.data; |
|
222 |
+ }) |
|
223 |
+ .catch(error => { |
|
224 |
+ console.error("fetchUnits - error: ", error); |
|
225 |
+ alert("단원 목록을 불러오는 중 오류가 발생했습니다."); |
|
226 |
+ }); |
|
227 |
+ }, |
|
228 |
+ |
|
142 | 229 |
}, |
143 | 230 |
watch: { |
144 | 231 |
|
145 | 232 |
}, |
146 | 233 |
computed: { |
147 |
- |
|
234 |
+ |
|
148 | 235 |
}, |
149 |
- components:{ |
|
236 |
+ components: { |
|
150 | 237 |
SvgIcon |
151 | 238 |
}, |
152 | 239 |
mounted() { |
153 |
- console.log('Main2 mounted'); |
|
240 |
+ this.selectedUnit = this.$route.query.unit_id || ''; |
|
241 |
+ this.selectedBook = this.$route.query.book_id || ''; |
|
242 |
+ |
|
243 |
+ this.fetchBooks(); |
|
244 |
+ this.fetchUnits(); |
|
245 |
+ |
|
246 |
+ if(this.selectedUnit){ |
|
247 |
+ this.fetchData(); |
|
248 |
+ } |
|
154 | 249 |
} |
155 | 250 |
} |
156 | 251 |
</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/teacher/QuestionDetail.vue
+++ client/views/pages/teacher/QuestionDetail.vue
... | ... | @@ -297,12 +297,23 @@ |
297 | 297 |
books: [], |
298 | 298 |
units: [], |
299 | 299 |
texts: [], |
300 |
+ file: '', |
|
301 |
+ selectedFiles: {}, |
|
300 | 302 |
} |
301 | 303 |
}, |
302 | 304 |
methods: { |
303 | 305 |
goToPage(page) { |
304 | 306 |
this.$router.push({ name: page }); |
305 | 307 |
}, |
308 |
+ |
|
309 |
+ handleFileUpload(e) { |
|
310 |
+ this.file = e.target.files; |
|
311 |
+ }, |
|
312 |
+ handleDetailFileUpload(index) { |
|
313 |
+ const files = this.$refs['fileInput' + index][0].files; |
|
314 |
+ this.selectedFiles[index] = files; |
|
315 |
+ }, |
|
316 |
+ |
|
306 | 317 |
problemSearch() { |
307 | 318 |
const vm = this; |
308 | 319 |
vm.prblm = JSON.parse(sessionStorage.getItem("selectQuestionList")); |
... | ... | @@ -405,6 +416,12 @@ |
405 | 416 |
fileMngId: this.file_mng_id // 첨부파일 ID |
406 | 417 |
}; |
407 | 418 |
|
419 |
+ if (this.file && this.file.length > 0) { |
|
420 |
+ // 파일 업로드를 수행하고, 결과로 얻은 fileMngId를 payload에 추가합니다. |
|
421 |
+ const fileMngId = await this.uploadFiles(this.file); |
|
422 |
+ payload.fileMngId = fileMngId; |
|
423 |
+ } |
|
424 |
+ |
|
408 | 425 |
try { |
409 | 426 |
const response = await axios.post('/problem/updateProblem.json', payload); |
410 | 427 |
console.log('성공:', response.data); |
... | ... | @@ -419,7 +436,7 @@ |
419 | 436 |
} |
420 | 437 |
}, |
421 | 438 |
|
422 |
- // 문제 상세 업로드 |
|
439 |
+ // 문제 상세 업데이트 |
|
423 | 440 |
async submitDetailForm(problemDetails) { |
424 | 441 |
|
425 | 442 |
try { |
... | ... | @@ -451,13 +468,19 @@ |
451 | 468 |
} |
452 | 469 |
|
453 | 470 |
for (let i = 1; i <= answerCount; i++) { |
471 |
+ if (this.selectedFiles[i] && this.selectedFiles[i].length > 0) { |
|
472 |
+ const uploadedFileMngId = await this.uploadFiles(this.selectedFiles[i]); |
|
473 |
+ answers[i].fileMngId = uploadedFileMngId; // 파일 매니지 ID 저장 |
|
474 |
+ } |
|
475 |
+ |
|
454 | 476 |
details.push({ |
455 |
- prblmDtlId: this.problemDetail[i - 1].prblmDtlId, |
|
477 |
+ prblmId, |
|
456 | 478 |
prblmDtlExpln: answers[i].text || '', |
457 | 479 |
prblmYn: answers[i].isCorrect, // isCorrect가 'Y' 또는 'N'으로 설정됨 |
458 | 480 |
fileMngId: answers[i].fileMngId, |
459 | 481 |
}); |
460 | 482 |
} |
483 |
+ |
|
461 | 484 |
|
462 | 485 |
return details; |
463 | 486 |
}, |
... | ... | @@ -487,7 +510,7 @@ |
487 | 510 |
|
488 | 511 |
// 문제 삭제 |
489 | 512 |
deletePost() { |
490 |
- const result = confirm('데이터를 삭제 하시겠습니까?') |
|
513 |
+ const result = confirm('문제를 삭제 하시겠습니까?') |
|
491 | 514 |
if (result) { |
492 | 515 |
|
493 | 516 |
} else { |
... | ... | @@ -501,15 +524,39 @@ |
501 | 524 |
} |
502 | 525 |
}) |
503 | 526 |
.then(response => { |
504 |
- alert(response.data.message); |
|
505 |
- this.goToPage('TextList'); |
|
527 |
+ |
|
528 |
+ this.goToPage('QuestionList'); |
|
506 | 529 |
}) |
507 | 530 |
.catch(error => { |
508 | 531 |
console.error("Error deleting post:", error); |
509 |
- alert("게시글 삭제에 오류가 발생했습니다."); |
|
532 |
+ alert("문제 삭제에 오류가 발생했습니다."); |
|
510 | 533 |
}); |
511 | 534 |
}, |
512 | 535 |
|
536 |
+ // 파일 업로드 메서드 |
|
537 |
+ async uploadFiles(files) { |
|
538 |
+ if (!files || files.length === 0) { |
|
539 |
+ return null; |
|
540 |
+ } |
|
541 |
+ |
|
542 |
+ const formData = new FormData(); |
|
543 |
+ for (let i = 0; i < files.length; i++) { |
|
544 |
+ formData.append("files", files[i]); |
|
545 |
+ } |
|
546 |
+ |
|
547 |
+ try { |
|
548 |
+ const response = await axios.post("/file/upload.json", formData, { |
|
549 |
+ headers: { |
|
550 |
+ "Content-Type": "multipart/form-data", |
|
551 |
+ }, |
|
552 |
+ }); |
|
553 |
+ |
|
554 |
+ return response.data.fileMngId; // 파일 매니지 ID 반환 |
|
555 |
+ } catch (error) { |
|
556 |
+ console.error("파일 업로드 오류:", error); |
|
557 |
+ throw new Error("파일 업로드에 실패했습니다."); |
|
558 |
+ } |
|
559 |
+ }, |
|
513 | 560 |
|
514 | 561 |
// 교재 정보 가져오기 |
515 | 562 |
fetchBooks() { |
--- client/views/pages/teacher/QuestionInsert.vue
+++ client/views/pages/teacher/QuestionInsert.vue
... | ... | @@ -69,7 +69,7 @@ |
69 | 69 |
</div> |
70 | 70 |
|
71 | 71 |
<div class="flex align-center"> |
72 |
- <label for="" class="title2">지문</label> |
|
72 |
+ <label for="" class="title2">교재</label> |
|
73 | 73 |
<select name="" id="" v-model="book_id" @change="fetchUnits" class="mr10"> |
74 | 74 |
<option value="" disabled>교재를 선택하세요</option> |
75 | 75 |
<option v-for="book in books" :key="book.book_id" :value="book.book_id"> |
--- client/views/pages/teacher/TextBookDetail.vue
+++ client/views/pages/teacher/TextBookDetail.vue
... | ... | @@ -107,7 +107,7 @@ |
107 | 107 |
<hr> |
108 | 108 |
<div class="mb20"> |
109 | 109 |
<div class="flex justify-between mb30 align-center"> |
110 |
- <label for="" class="title1">중간 평가</label> |
|
110 |
+ <label for="" class="title1">중간 / 최종</label> |
|
111 | 111 |
<div class="look-btn flex align-center" @click="goToPage('ExamList')"> |
112 | 112 |
<p>자세히 보기 </p> |
113 | 113 |
<svg-icon type="mdi" :path="mdilArrowRight" class=" ml10"></svg-icon> |
... | ... | @@ -116,54 +116,21 @@ |
116 | 116 |
<div class="table-wrap"> |
117 | 117 |
<table> |
118 | 118 |
<thead> |
119 |
- <td>No.</td> |
|
120 |
- <td>제목</td> |
|
121 |
- <td>내용</td> |
|
122 |
- <td>작성자</td> |
|
123 |
- <td>등록일</td> |
|
124 |
- </thead> |
|
125 |
- <tbody> |
|
126 |
- <tr> |
|
127 |
- <td></td> |
|
128 |
- <td></td> |
|
129 |
- <td></td> |
|
130 |
- <td></td> |
|
131 |
- <td></td> |
|
132 |
- </tr> |
|
133 |
- </tbody> |
|
119 |
+ <td>No.</td> |
|
120 |
+ <td>중간/최종</td> |
|
121 |
+ <td>문항 갯수</td> |
|
122 |
+ </thead> |
|
123 |
+ <tbody> |
|
124 |
+ <tr v-for="(post, index) in evals" :key="post.evalId"> |
|
125 |
+ <td>{{ index + 1 }}</td> |
|
126 |
+ <td>{{ post.evalType }}</td> |
|
127 |
+ <td>{{ post.problemCount }}</td> |
|
128 |
+ </tr> |
|
129 |
+ </tbody> |
|
134 | 130 |
</table> |
135 | 131 |
</div> |
136 | 132 |
</div> |
137 | 133 |
<hr> |
138 |
- <div> |
|
139 |
- <div class="flex justify-between mb30 align-center"> |
|
140 |
- <label for="" class="title1">최종 평가</label> |
|
141 |
- <div class="look-btn flex align-center" @click="goToPage('ExamList')"> |
|
142 |
- <p>자세히 보기 </p> |
|
143 |
- <svg-icon type="mdi" :path="mdilArrowRight" class=" ml10"></svg-icon> |
|
144 |
- </div> |
|
145 |
- </div> |
|
146 |
- <div class="table-wrap"> |
|
147 |
- <table> |
|
148 |
- <thead> |
|
149 |
- <td>No.</td> |
|
150 |
- <td>제목</td> |
|
151 |
- <td>내용</td> |
|
152 |
- <td>작성자</td> |
|
153 |
- <td>등록일</td> |
|
154 |
- </thead> |
|
155 |
- <tbody> |
|
156 |
- <tr> |
|
157 |
- <td></td> |
|
158 |
- <td></td> |
|
159 |
- <td></td> |
|
160 |
- <td></td> |
|
161 |
- <td></td> |
|
162 |
- </tr> |
|
163 |
- </tbody> |
|
164 |
- </table> |
|
165 |
- </div> |
|
166 |
- </div> |
|
167 | 134 |
</div> |
168 | 135 |
<div class="flex justify-end mt30" style="gap: 10px;"> |
169 | 136 |
<button type="button" title="" class="new-btn" @click="goToPage('RoadMap')"> |
... | ... | @@ -220,6 +187,7 @@ |
220 | 187 |
units: [], |
221 | 188 |
posts: [], |
222 | 189 |
quizs: [], |
190 |
+ evals: [], |
|
223 | 191 |
dataList: [], |
224 | 192 |
totalPosts: [], |
225 | 193 |
selectedUnitId: null, // 선택된 단원 ID 저장 변수 |
... | ... | @@ -311,7 +279,7 @@ |
311 | 279 |
this.findText(); |
312 | 280 |
this.findProblem(); |
313 | 281 |
this.findWord(); |
314 |
- |
|
282 |
+ this.fetchEvalData(); |
|
315 | 283 |
}) |
316 | 284 |
.catch(error => { |
317 | 285 |
console.error('Error selectUnit details:', error); |
... | ... | @@ -326,6 +294,7 @@ |
326 | 294 |
this.findText(); |
327 | 295 |
this.findProblem(); |
328 | 296 |
this.findWord(); |
297 |
+ this.fetchEvalData(); |
|
329 | 298 |
}, |
330 | 299 |
|
331 | 300 |
// 지문 정보 5개 정도 가져오기 |
... | ... | @@ -439,6 +408,29 @@ |
439 | 408 |
return wordString; |
440 | 409 |
} |
441 | 410 |
}, |
411 |
+ // 평가 정보 5개 정도 가져오기 |
|
412 |
+ fetchEvalData() { |
|
413 |
+ axios({ |
|
414 |
+ url: "/evaluation/evaluationUnitList.json", |
|
415 |
+ method: "post", |
|
416 |
+ headers: { |
|
417 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
418 |
+ }, |
|
419 |
+ data: { |
|
420 |
+ "pageSize": 5, |
|
421 |
+ "startIndex": 0, |
|
422 |
+ "unitId": this.selectedUnitId |
|
423 |
+ }, |
|
424 |
+ }) |
|
425 |
+ .then(response => { |
|
426 |
+ this.evals = response.data; |
|
427 |
+ }) |
|
428 |
+ .catch(error => { |
|
429 |
+ console.error("fetchData - error: ", error); |
|
430 |
+ alert("검색 중 오류가 발생했습니다."); |
|
431 |
+ }); |
|
432 |
+ }, |
|
433 |
+ |
|
442 | 434 |
|
443 | 435 |
}, |
444 | 436 |
mounted() { |
--- client/views/pages/teacher/TextDetail.vue
+++ client/views/pages/teacher/TextDetail.vue
... | ... | @@ -53,7 +53,8 @@ |
53 | 53 |
<hr> |
54 | 54 |
<div class="flex align-center"> |
55 | 55 |
<label for="" class="title2">첨부파일</label> |
56 |
- <label for="" class="title2" v-if="file">{{ file.fileNm }}</label> |
|
56 |
+ <input v-if="isEditing" type="file" ref="fileInput" @change="handleFileUpload" multiple /> |
|
57 |
+ <label v-else for="" class="title2">{{ file.fileNm }}</label> |
|
57 | 58 |
</div> |
58 | 59 |
|
59 | 60 |
</div> |
... | ... | @@ -95,8 +96,9 @@ |
95 | 96 |
}, |
96 | 97 |
books: [], |
97 | 98 |
units: [], |
98 |
- file: {}, |
|
99 |
- isEditing: false |
|
99 |
+ file: { fileNm : '' }, |
|
100 |
+ isEditing: false, |
|
101 |
+ selectedFiles: {}, |
|
100 | 102 |
}; |
101 | 103 |
}, |
102 | 104 |
computed: { |
... | ... | @@ -116,6 +118,12 @@ |
116 | 118 |
goToPage(page) { |
117 | 119 |
this.$router.push({ name: page }); |
118 | 120 |
}, |
121 |
+ |
|
122 |
+ handleFileUpload(e) { |
|
123 |
+ this.selectedFiles = e.target.files; |
|
124 |
+ }, |
|
125 |
+ |
|
126 |
+ |
|
119 | 127 |
fetchPostDetail() { |
120 | 128 |
const textId = this.$route.query.textId; |
121 | 129 |
axios.post('/text/selectOneText.json', { textId }, { |
... | ... | @@ -135,7 +143,9 @@ |
135 | 143 |
this.post.bookName = response.data[0].book_name; |
136 | 144 |
this.post.unitName = response.data[0].unit_name; |
137 | 145 |
this.post.fileMngId = response.data[0].file_mng_id; |
138 |
- this.findFile(); |
|
146 |
+ if(this.post.fileMngId) { |
|
147 |
+ this.findFile(); |
|
148 |
+ } |
|
139 | 149 |
} else { |
140 | 150 |
this.error = "Failed to fetch post details."; |
141 | 151 |
} |
... | ... | @@ -167,7 +177,7 @@ |
167 | 177 |
}); |
168 | 178 |
}, |
169 | 179 |
|
170 |
- dataInsert() { |
|
180 |
+ async dataInsert() { |
|
171 | 181 |
this.newPost = { |
172 | 182 |
textId: this.post.textId, |
173 | 183 |
textTtl: this.post.textTtl, |
... | ... | @@ -178,6 +188,29 @@ |
178 | 188 |
unitId: this.post.unitId, |
179 | 189 |
fileMngId: this.post.fileMngId, |
180 | 190 |
}; |
191 |
+ |
|
192 |
+ if (this.selectedFiles && this.selectedFiles.length > 0) { |
|
193 |
+ // 파일 업로드 |
|
194 |
+ const formData = new FormData(); |
|
195 |
+ for (let i = 0; i < this.selectedFiles.length; i++) { |
|
196 |
+ formData.append("files", this.selectedFiles[i]); |
|
197 |
+ } |
|
198 |
+ |
|
199 |
+ const fileUploadResponse = await axios.post( |
|
200 |
+ "/file/upload.json", |
|
201 |
+ formData, |
|
202 |
+ { |
|
203 |
+ headers: { |
|
204 |
+ "Content-Type": "multipart/form-data", |
|
205 |
+ }, |
|
206 |
+ } |
|
207 |
+ ); |
|
208 |
+ |
|
209 |
+ // 업로드 후 파일 매니지 아이디 호출 |
|
210 |
+ vm.newPost.fileMngId = fileUploadResponse.data.fileMngId; |
|
211 |
+ } |
|
212 |
+ |
|
213 |
+ |
|
181 | 214 |
console.log(this.newPost) |
182 | 215 |
|
183 | 216 |
axios.post("/text/textUpdate.json", this.newPost, { |
... | ... | @@ -191,7 +224,7 @@ |
191 | 224 |
}) |
192 | 225 |
.catch(error => { |
193 | 226 |
console.log("dataInsert - error:", error.response.data); |
194 |
- alert("게시글 등록에 오류가 발생했습니다."); |
|
227 |
+ alert("지문 업데이트에 오류가 발생했습니다."); |
|
195 | 228 |
}); |
196 | 229 |
}, |
197 | 230 |
toggleEdit() { |
--- client/views/pages/teacher/VocaDetail.vue
+++ client/views/pages/teacher/VocaDetail.vue
... | ... | @@ -2,19 +2,66 @@ |
2 | 2 |
<div class="title-box flex justify-between mb40"> |
3 | 3 |
<p class="title">단어장 수정</p> |
4 | 4 |
</div> |
5 |
- <div class="board-wrap"> |
|
6 |
- <div class="flex align-center mb20"> |
|
7 |
- <label for="" class="title2">지문</label> |
|
8 |
- <select v-model="selectedSearchOption" class="mr10 data-wrap"> |
|
9 |
- <option value="bbsTtl">제목</option> |
|
10 |
- <option value="bbsCnt">내용</option> |
|
11 |
- <option value="userNm">작성자</option> |
|
12 |
- <option value="bbsCls">카테고리</option> |
|
13 |
- </select> |
|
14 |
- </div> |
|
15 |
- <div class="flex align-center"> |
|
16 |
- <label for="" class="title2">단어 목록</label> |
|
17 |
- <div class="flex-column" style="gap: 10px;"> |
|
5 |
+ <div class="content-t"> |
|
6 |
+ |
|
7 |
+ <div class="board-wrap"> |
|
8 |
+ <div class="flex align-center mb20"> |
|
9 |
+ <div class="flex align-center"> |
|
10 |
+ <label for="" class="title2">교재</label> |
|
11 |
+ <select name="" id="" v-model="selectedBookId" @change="fetchUnits" class="mr10"> |
|
12 |
+ <option value="" disabled>교재를 선택하세요</option> |
|
13 |
+ <option v-for="book in books" :key="book.book_id" :value="book.book_id"> |
|
14 |
+ {{ book.book_nm }} |
|
15 |
+ </option> |
|
16 |
+ </select> |
|
17 |
+ <select name="" id="" v-model="selectedUnitId" @change="fetchTexts" class="mr10"> |
|
18 |
+ <option value="" disabled>단원을 선택하세요</option> |
|
19 |
+ <option v-for="unit in units" :key="unit.unitId" :value="unit.unitId"> |
|
20 |
+ {{ unit.unitName }} |
|
21 |
+ </option> |
|
22 |
+ </select> |
|
23 |
+ |
|
24 |
+ <select v-model="selectedTextId" class="mr10 data-wrap"> |
|
25 |
+ <option value="" disabled>지문을 선택하세요</option> |
|
26 |
+ <option v-for="text in texts" :key="text.textId" :value="text.textId"> |
|
27 |
+ {{ text.textTtl }} |
|
28 |
+ </option> |
|
29 |
+ </select> |
|
30 |
+ </div> |
|
31 |
+ </div> |
|
32 |
+ <div class="flex align-center mb20"> |
|
33 |
+ <label for="" class="title2">단어장 타입</label> |
|
34 |
+ <select v-model="selectedWdBookTypeId" class="mr10 data-wrap"> |
|
35 |
+ <option value="1">단어장 (일반)</option> |
|
36 |
+ <option value="2">단어장 (스피킹)</option> |
|
37 |
+ <option value="3">단어장 (숏폼)</option> |
|
38 |
+ <option value="4">단어장 (카드 뒤집기)</option> |
|
39 |
+ <option value="5">단어장 (카드 맞추기)</option> |
|
40 |
+ </select> |
|
41 |
+ </div> |
|
42 |
+ <div class="flex align-center"> |
|
43 |
+ <label for="" class="title2">단어 목록</label> |
|
44 |
+ <div class="flex-column" style="gap: 10px;"> |
|
45 |
+ <div class="flex align-center" style="gap: 10px;"> |
|
46 |
+ <input v-model="newWord.eng" type="text" class="data-wrap" placeholder="영어"> |
|
47 |
+ <input v-model="newWord.kor" type="text" class="data-wrap" placeholder="한글"> |
|
48 |
+ <input type="file" ref="fileInput" @change="handleFileUpload" multiple /> |
|
49 |
+ <button type="button" @click="addWord"> |
|
50 |
+ <img src="../../../resources/img/btn39_120t_normal.png" alt=""> |
|
51 |
+ </button> |
|
52 |
+ </div> |
|
53 |
+ |
|
54 |
+ <!-- 여기에 단어장에 소속될 단어들 태그 형태 리스트 --> |
|
55 |
+ <div v-for="(word, index) in words" :key="index" class="flex align-center" style="gap: 10px;"> |
|
56 |
+ <input v-model="word.eng" type="text" class="data-wrap" placeholder="영어"> |
|
57 |
+ <input v-model="word.kor" type="text" class="data-wrap" placeholder="한글"> |
|
58 |
+ <button type="button" @click="removeWord(index)"> |
|
59 |
+ <img src="../../../resources/img/btn38_120t_normal.png" alt=""> |
|
60 |
+ </button> |
|
61 |
+ </div> |
|
62 |
+ |
|
63 |
+ </div> |
|
64 |
+ <!-- |
|
18 | 65 |
<div class="flex align-center" style="gap: 10px;"> |
19 | 66 |
<input type="text" class="data-wrap" placeholder="영어"> |
20 | 67 |
<input type="text" class="data-wrap" placeholder="한글"> |
... | ... | @@ -31,21 +78,22 @@ |
31 | 78 |
</button> |
32 | 79 |
|
33 | 80 |
</div> |
34 |
- </div> |
|
35 | 81 |
|
82 |
+ --> |
|
83 |
+ </div> |
|
36 | 84 |
</div> |
37 |
- </div> |
|
38 |
- <div class="flex justify-between mt50"> |
|
39 |
- <button type="button" title="글쓰기" class="new-btn" @click="goToPage('VocaList')"> |
|
40 |
- 목록 |
|
41 |
- </button> |
|
42 |
- <div class="flex"> |
|
43 |
- <button type="button" title="글쓰기" class="new-btn mr10"> |
|
44 |
- 취소 |
|
85 |
+ <div class="flex justify-between mt50"> |
|
86 |
+ <button type="button" title="목록" class="new-btn" @click="goToPage('VocaList')"> |
|
87 |
+ 목록 |
|
45 | 88 |
</button> |
46 |
- <button type="button" title="글쓰기" class="new-btn"> |
|
47 |
- 수정 |
|
48 |
- </button> |
|
89 |
+ <div class="flex"> |
|
90 |
+ <button type="button" title="취소" class="new-btn mr10" @click="cancelAction"> |
|
91 |
+ 취소 |
|
92 |
+ </button> |
|
93 |
+ <button type="button" title="수정" class="new-btn" @click="registerWordBook"> |
|
94 |
+ 수정 |
|
95 |
+ </button> |
|
96 |
+ </div> |
|
49 | 97 |
</div> |
50 | 98 |
</div> |
51 | 99 |
</template> |
... | ... | @@ -53,6 +101,7 @@ |
53 | 101 |
<script> |
54 | 102 |
import SvgIcon from '@jamescoyle/vue-icon'; |
55 | 103 |
import { mdiMagnify, mdiPlusCircleOutline, mdiWindowClose } from '@mdi/js'; |
104 |
+import axios from "axios"; |
|
56 | 105 |
|
57 | 106 |
|
58 | 107 |
export default { |
... | ... | @@ -62,9 +111,31 @@ |
62 | 111 |
mdiPlusCircleOutline: mdiPlusCircleOutline, |
63 | 112 |
mdiMagnify: mdiMagnify, |
64 | 113 |
mdiWindowClose: mdiWindowClose, |
114 |
+ |
|
115 |
+ selectedBookId: null, |
|
116 |
+ selectedUnitId: null, |
|
117 |
+ selectedTextId: null, |
|
118 |
+ |
|
119 |
+ newWord: { eng: '', kor: '', fileMngId: null }, // 입력된 새 단어 |
|
120 |
+ words: [], // 단어 목록 |
|
121 |
+ |
|
122 |
+ books: [], |
|
123 |
+ units: [], |
|
124 |
+ texts: [], |
|
125 |
+ |
|
126 |
+ wordbooks: [], |
|
127 |
+ selectedWdBookId: null, |
|
65 | 128 |
} |
66 | 129 |
}, |
67 | 130 |
methods: { |
131 |
+ async handleFileUpload(e) { |
|
132 |
+ const files = e.target.files; |
|
133 |
+ if (files.length > 0) { |
|
134 |
+ const fileMngId = await this.uploadFiles(files); |
|
135 |
+ this.newWord.fileMngId = fileMngId; // 파일 매니지 ID 저장 |
|
136 |
+ } |
|
137 |
+ }, |
|
138 |
+ |
|
68 | 139 |
// 논문실적 버튼 추가 |
69 | 140 |
addThesis() { |
70 | 141 |
// 고유 ID로 현재 시간의 타임스탬프를 사용 |
... | ... | @@ -82,9 +153,280 @@ |
82 | 153 |
} |
83 | 154 |
}, |
84 | 155 |
// |
156 |
+ |
|
157 |
+ addWord() { // 단어 추가 |
|
158 |
+ if (this.newWord.eng && this.newWord.kor) { |
|
159 |
+ const newWordWithFile = { ...this.newWord }; // 새 단어 객체 |
|
160 |
+ this.words.push(newWordWithFile); |
|
161 |
+ // 초기화 |
|
162 |
+ this.newWord.eng = ''; |
|
163 |
+ this.newWord.kor = ''; |
|
164 |
+ this.newWord.fileMngId = null; // 파일 매니지 ID 초기화 |
|
165 |
+ } else { |
|
166 |
+ console.log("단어 입력이 비어 있음"); |
|
167 |
+ } |
|
168 |
+ }, |
|
169 |
+ |
|
170 |
+ removeWord(index) { // 단어 삭제 |
|
171 |
+ this.words.splice(index, 1); |
|
172 |
+ }, |
|
173 |
+ |
|
85 | 174 |
goToPage(page) { |
86 | 175 |
this.$router.push({ name: page }); |
87 | 176 |
}, |
177 |
+ |
|
178 |
+ cancelAction() { |
|
179 |
+ this.$router.go(-1); |
|
180 |
+ }, |
|
181 |
+ |
|
182 |
+ |
|
183 |
+ // 교재 정보 가져오기 |
|
184 |
+ fetchBooks() { |
|
185 |
+ axios({ |
|
186 |
+ url: "/book/findAll.json", |
|
187 |
+ method: "post", |
|
188 |
+ headers: { |
|
189 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
190 |
+ }, |
|
191 |
+ }) |
|
192 |
+ .then(response => { |
|
193 |
+ console.log(response.data) |
|
194 |
+ this.books = response.data; |
|
195 |
+ this.selectedTextId = ''; |
|
196 |
+ }) |
|
197 |
+ .catch(error => { |
|
198 |
+ console.error("fetchBooks - error: ", error); |
|
199 |
+ alert("교재 목록을 불러오는 중 오류가 발생했습니다."); |
|
200 |
+ }); |
|
201 |
+ }, |
|
202 |
+ |
|
203 |
+ // 단원 정보 가져오기 |
|
204 |
+ fetchUnits() { |
|
205 |
+ if (!this.selectedBookId) return; |
|
206 |
+ |
|
207 |
+ axios({ |
|
208 |
+ url: "/unit/unitList.json", |
|
209 |
+ method: "post", |
|
210 |
+ headers: { |
|
211 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
212 |
+ }, |
|
213 |
+ data: { |
|
214 |
+ "bookId": this.selectedBookId |
|
215 |
+ }, |
|
216 |
+ }) |
|
217 |
+ .then(response => { |
|
218 |
+ this.units = response.data; |
|
219 |
+ this.selectedTextId = ''; |
|
220 |
+ this.fetchTexts(); |
|
221 |
+ }) |
|
222 |
+ .catch(error => { |
|
223 |
+ console.error("fetchUnits - error: ", error); |
|
224 |
+ alert("단원 목록을 불러오는 중 오류가 발생했습니다."); |
|
225 |
+ }); |
|
226 |
+ }, |
|
227 |
+ |
|
228 |
+ // 지문 정보 가져오기 |
|
229 |
+ fetchTexts() { |
|
230 |
+ axios({ |
|
231 |
+ url: "/text/textSearch.json", |
|
232 |
+ method: "post", |
|
233 |
+ headers: { |
|
234 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
235 |
+ }, |
|
236 |
+ data: { |
|
237 |
+ "unitId": this.selectedUnitId |
|
238 |
+ }, |
|
239 |
+ }) |
|
240 |
+ .then(response => { |
|
241 |
+ this.texts = response.data.list; |
|
242 |
+ }) |
|
243 |
+ .catch(error => { |
|
244 |
+ console.error("fetchTexts - error: ", error); |
|
245 |
+ alert("지문 목록을 불러오는 중 오류가 발생했습니다."); |
|
246 |
+ }); |
|
247 |
+ }, |
|
248 |
+ |
|
249 |
+ // 단어장의 단어 정보 조회 메서드 |
|
250 |
+ dataSelectList() { |
|
251 |
+ const vm = this; |
|
252 |
+ axios({ |
|
253 |
+ url: "/word/getWordsByBookId.json", |
|
254 |
+ method: "post", |
|
255 |
+ headers: { |
|
256 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
257 |
+ }, |
|
258 |
+ data: { |
|
259 |
+ wdBookId: vm.selectedWdBookId, |
|
260 |
+ }, |
|
261 |
+ }) |
|
262 |
+ .then(function (response) { |
|
263 |
+ console.log("dataList - response: ", response.data); |
|
264 |
+ vm.words = response.data.map(word => ({ |
|
265 |
+ eng: word.wdNm, |
|
266 |
+ kor: word.wdMnng, |
|
267 |
+ fileMngId: word.fileMngId // 필요시 fileMngId도 포함 |
|
268 |
+ })); |
|
269 |
+ |
|
270 |
+ }) |
|
271 |
+ .catch(function (error) { |
|
272 |
+ console.log("dataList - error: ", error); |
|
273 |
+ alert("단어장 목록 조회에 오류가 발생했습니다."); |
|
274 |
+ }); |
|
275 |
+ }, |
|
276 |
+ |
|
277 |
+ // 기존 단어 조회 메서드 |
|
278 |
+ fetchExistingWords(wdBookId) { |
|
279 |
+ return axios.post('/word/getWordsByBookId.json', { wdBookId: wdBookId }) |
|
280 |
+ .then(response => { |
|
281 |
+ console.log("기존 단어 ", response.data); |
|
282 |
+ return response.data; |
|
283 |
+ }) |
|
284 |
+ .catch(error => { |
|
285 |
+ console.error('기존 단어 목록 가져오기 실패:', error); |
|
286 |
+ return []; |
|
287 |
+ }); |
|
288 |
+ }, |
|
289 |
+ // 파일 업로드 메서드 |
|
290 |
+ async uploadFiles(files) { |
|
291 |
+ if (!files || files.length === 0) { |
|
292 |
+ return null; |
|
293 |
+ } |
|
294 |
+ |
|
295 |
+ const formData = new FormData(); |
|
296 |
+ for (let i = 0; i < files.length; i++) { |
|
297 |
+ formData.append("files", files[i]); |
|
298 |
+ } |
|
299 |
+ |
|
300 |
+ try { |
|
301 |
+ const response = await axios.post("/file/upload.json", formData, { |
|
302 |
+ headers: { |
|
303 |
+ "Content-Type": "multipart/form-data", |
|
304 |
+ }, |
|
305 |
+ }); |
|
306 |
+ |
|
307 |
+ return response.data.fileMngId; // 파일 매니지 ID 반환 |
|
308 |
+ } catch (error) { |
|
309 |
+ console.error("파일 업로드 오류:", error); |
|
310 |
+ throw new Error("파일 업로드에 실패했습니다."); |
|
311 |
+ } |
|
312 |
+ }, |
|
313 |
+ |
|
314 |
+ |
|
315 |
+ async registerWordBook() { |
|
316 |
+ const vm = this; |
|
317 |
+ |
|
318 |
+ try { |
|
319 |
+ const response = await axios.post('/wordbook/update.json', { |
|
320 |
+ wdBookTypeId: vm.selectedWdBookTypeId, |
|
321 |
+ textId: vm.selectedTextId, |
|
322 |
+ userId: vm.wordbooks.userId, |
|
323 |
+ bookId: vm.selectedBookId, |
|
324 |
+ unitId: vm.selectedUnitId, |
|
325 |
+ wdBookId: vm.selectedWdBookId, |
|
326 |
+ }); |
|
327 |
+ |
|
328 |
+ const wdBookId = vm.selectedWdBookId; |
|
329 |
+ const existingWords = await vm.fetchExistingWords(wdBookId); |
|
330 |
+ console.log('기존 단어 목록:', existingWords); |
|
331 |
+ |
|
332 |
+ const existingWordMap = new Map(existingWords.map(word => [word.wdNm, word])); |
|
333 |
+ const wordsToInsert = []; |
|
334 |
+ const wordsToUpdate = []; |
|
335 |
+ const wordsToDelete = []; |
|
336 |
+ |
|
337 |
+ // 신규 단어 처리 |
|
338 |
+ vm.words.forEach(word => { |
|
339 |
+ const existingWord = existingWordMap.get(word.eng); |
|
340 |
+ if (existingWord) { |
|
341 |
+ wordsToUpdate.push({ |
|
342 |
+ wdId: existingWord.wdId, |
|
343 |
+ wdBookId: wdBookId, |
|
344 |
+ wdNm: existingWord.wdNm, |
|
345 |
+ wdMnng: word.kor, |
|
346 |
+ fileMngId: word.fileMngId, |
|
347 |
+ }); |
|
348 |
+ } else { |
|
349 |
+ wordsToInsert.push({ |
|
350 |
+ wdBookId: wdBookId, |
|
351 |
+ wdNm: word.eng, |
|
352 |
+ wdMnng: word.kor, |
|
353 |
+ fileMngId: word.fileMngId |
|
354 |
+ }); |
|
355 |
+ } |
|
356 |
+ }); |
|
357 |
+ |
|
358 |
+ // 삭제할 단어 처리 |
|
359 |
+ existingWords.forEach(word => { |
|
360 |
+ if (!vm.words.find(newWord => newWord.eng === word.wdNm)) { |
|
361 |
+ wordsToDelete.push(word); |
|
362 |
+ } |
|
363 |
+ }); |
|
364 |
+ |
|
365 |
+ console.log('신규 단어 목록:', wordsToInsert); |
|
366 |
+ console.log('업데이트 단어 목록:', wordsToUpdate); |
|
367 |
+ console.log('삭제할 단어 목록:', wordsToDelete); |
|
368 |
+ |
|
369 |
+ // 단어 삽입 |
|
370 |
+ for (const word of wordsToInsert) { |
|
371 |
+ const insertResponse = await axios.post('/word/insert.json', word); |
|
372 |
+ console.log('삽입 응답:', insertResponse); |
|
373 |
+ } |
|
374 |
+ |
|
375 |
+ // 단어 업데이트 |
|
376 |
+ for (const word of wordsToUpdate) { |
|
377 |
+ const updateResponse = await axios.post('/word/update.json', { |
|
378 |
+ wdId: word.wdId, |
|
379 |
+ wdBookId: wdBookId, |
|
380 |
+ wdNm: word.wdNm, |
|
381 |
+ wdMnng: word.wdMnng, |
|
382 |
+ fileMngId: word.fileMngId, |
|
383 |
+ }); |
|
384 |
+ console.log('업데이트 응답:', updateResponse); |
|
385 |
+ } |
|
386 |
+ |
|
387 |
+ // 단어 삭제 |
|
388 |
+ for (const word of wordsToDelete) { |
|
389 |
+ const deleteResponse = await axios.post('/word/delete.json', { |
|
390 |
+ wdBookId: wdBookId, |
|
391 |
+ wdId: word.wdId |
|
392 |
+ }); |
|
393 |
+ console.log('삭제 응답:', deleteResponse); |
|
394 |
+ } |
|
395 |
+ |
|
396 |
+ alert('단어장이 성공적으로 등록되었습니다.'); |
|
397 |
+ vm.goToPage('VocaList'); |
|
398 |
+ } catch (error) { |
|
399 |
+ console.error('단어장 등록 중 오류 발생:', error); |
|
400 |
+ alert('단어장 등록에 실패했습니다.'); |
|
401 |
+ } |
|
402 |
+ }, |
|
403 |
+ |
|
404 |
+ // 단어장 정보 가져오기 |
|
405 |
+ wordBookFind() { |
|
406 |
+ const vm = this; |
|
407 |
+ axios({ |
|
408 |
+ url: "/wordbook/find.json", |
|
409 |
+ method: "post", |
|
410 |
+ headers: { |
|
411 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
412 |
+ }, |
|
413 |
+ data: { |
|
414 |
+ wdBookId: vm.selectedWdBookId, |
|
415 |
+ }, |
|
416 |
+ }) |
|
417 |
+ .then(function (response) { |
|
418 |
+ console.log("dataList - response: ", response.data); |
|
419 |
+ vm.selectedWdBookTypeId = response.data.wdBookTypeId; |
|
420 |
+ vm.selectedTextId = response.data.textId; |
|
421 |
+ vm.wordbooks = response.data; |
|
422 |
+ }) |
|
423 |
+ .catch(function (error) { |
|
424 |
+ console.log("dataList - error: ", error); |
|
425 |
+ alert("단어장 목록 조회에 오류가 발생했습니다."); |
|
426 |
+ }); |
|
427 |
+ }, |
|
428 |
+ |
|
429 |
+ |
|
88 | 430 |
}, |
89 | 431 |
watch: { |
90 | 432 |
|
... | ... | @@ -96,7 +438,14 @@ |
96 | 438 |
SvgIcon |
97 | 439 |
}, |
98 | 440 |
mounted() { |
99 |
- console.log('Main2 mounted'); |
|
441 |
+ this.selectedBookId = this.$route.query.selectedBookId || ''; |
|
442 |
+ this.selectedUnitId = this.$route.query.selectedUnitId || ''; |
|
443 |
+ this.selectedWdBookId = this.$route.query.wdBookId || ''; |
|
444 |
+ this.fetchBooks(); |
|
445 |
+ this.fetchUnits(); |
|
446 |
+ |
|
447 |
+ this.dataSelectList(); |
|
448 |
+ this.wordBookFind(); |
|
100 | 449 |
} |
101 | 450 |
} |
102 | 451 |
</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/teacher/VocaInsert.vue
+++ client/views/pages/teacher/VocaInsert.vue
... | ... | @@ -5,12 +5,28 @@ |
5 | 5 |
<div class="title2 gray flex mb40">{{ titleMessage }}</div> |
6 | 6 |
<div class="board-wrap"> |
7 | 7 |
<div class="flex align-center mb20"> |
8 |
- <label for="" class="title2">지문</label> |
|
9 |
- <select v-model="selectedTextId" class="mr10 data-wrap"> |
|
10 |
- <option v-for="text in texts" :key="text.textId" :value="text.textId"> |
|
11 |
- {{ text.textTtl }} |
|
12 |
- </option> |
|
13 |
- </select> |
|
8 |
+ <div class="flex align-center"> |
|
9 |
+ <label for="" class="title2">교재</label> |
|
10 |
+ <select name="" id="" v-model="selectedBookId" @change="fetchUnits" class="mr10"> |
|
11 |
+ <option value="" disabled>교재를 선택하세요</option> |
|
12 |
+ <option v-for="book in books" :key="book.book_id" :value="book.book_id"> |
|
13 |
+ {{ book.book_nm }} |
|
14 |
+ </option> |
|
15 |
+ </select> |
|
16 |
+ <select name="" id="" v-model="selectedUnitId" @change="fetchTexts" class="mr10"> |
|
17 |
+ <option value="" disabled>단원을 선택하세요</option> |
|
18 |
+ <option v-for="unit in units" :key="unit.unitId" :value="unit.unitId"> |
|
19 |
+ {{ unit.unitName }} |
|
20 |
+ </option> |
|
21 |
+ </select> |
|
22 |
+ |
|
23 |
+ <select v-model="selectedTextId" class="mr10 data-wrap"> |
|
24 |
+ <option value="" disabled>지문을 선택하세요</option> |
|
25 |
+ <option v-for="text in texts" :key="text.textId" :value="text.textId"> |
|
26 |
+ {{ text.textTtl }} |
|
27 |
+ </option> |
|
28 |
+ </select> |
|
29 |
+ </div> |
|
14 | 30 |
</div> |
15 | 31 |
<div class="flex align-center mb20"> |
16 | 32 |
<label for="" class="title2">단어장 타입</label> |
... | ... | @@ -28,6 +44,7 @@ |
28 | 44 |
<div class="flex align-center" style="gap: 10px;"> |
29 | 45 |
<input v-model="newWord.eng" type="text" class="data-wrap" placeholder="영어"> |
30 | 46 |
<input v-model="newWord.kor" type="text" class="data-wrap" placeholder="한글"> |
47 |
+ <input type="file" ref="fileInput" @change="handleFileUpload" multiple /> |
|
31 | 48 |
<button type="button" @click="addWord"> |
32 | 49 |
<img src="../../../resources/img/btn39_120t_normal.png" alt=""> |
33 | 50 |
</button> |
... | ... | @@ -73,13 +90,72 @@ |
73 | 90 |
texts: [], // 지문 목록 |
74 | 91 |
selectedTextId: null, // 선택된 지문 ID |
75 | 92 |
selectedWdBookTypeId: '1', // 선택된 단어장 타입 ID |
76 |
- newWord: { eng: '', kor: '' }, // 입력된 새 단어 |
|
93 |
+ newWord: { eng: '', kor: '', fileMngId: null }, // 입력된 새 단어 |
|
77 | 94 |
words: [], // 단어 목록 |
95 |
+ books: [], |
|
96 |
+ units: [], |
|
78 | 97 |
existingWords: [], // 기존 단어 목록 저장 |
79 |
- userId: "2" |
|
98 |
+ userId: "USID_000000000000004", |
|
99 |
+ |
|
100 |
+ file: '', |
|
101 |
+ selectedFiles: [], |
|
102 |
+ |
|
80 | 103 |
} |
81 | 104 |
}, |
82 | 105 |
methods: { |
106 |
+ async handleFileUpload(e) { |
|
107 |
+ const files = e.target.files; |
|
108 |
+ if (files.length > 0) { |
|
109 |
+ const fileMngId = await this.uploadFiles(files); |
|
110 |
+ this.newWord.fileMngId = fileMngId; // 파일 매니지 ID 저장 |
|
111 |
+ } |
|
112 |
+ }, |
|
113 |
+ |
|
114 |
+ // 교재 정보 가져오기 |
|
115 |
+ fetchBooks() { |
|
116 |
+ this.selectedUnitId = ''; |
|
117 |
+ this.selectedTextId = ''; |
|
118 |
+ axios({ |
|
119 |
+ url: "/book/findAll.json", |
|
120 |
+ method: "post", |
|
121 |
+ headers: { |
|
122 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
123 |
+ }, |
|
124 |
+ }) |
|
125 |
+ .then(response => { |
|
126 |
+ console.log(response.data) |
|
127 |
+ this.books = response.data; |
|
128 |
+ this.selectedTextId = ''; |
|
129 |
+ }) |
|
130 |
+ .catch(error => { |
|
131 |
+ console.error("fetchBooks - error: ", error); |
|
132 |
+ alert("교재 목록을 불러오는 중 오류가 발생했습니다."); |
|
133 |
+ }); |
|
134 |
+ }, |
|
135 |
+ |
|
136 |
+ // 단원 정보 가져오기 |
|
137 |
+ fetchUnits() { |
|
138 |
+ if (!this.selectedBookId) return; |
|
139 |
+ |
|
140 |
+ axios({ |
|
141 |
+ url: "/unit/unitList.json", |
|
142 |
+ method: "post", |
|
143 |
+ headers: { |
|
144 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
145 |
+ }, |
|
146 |
+ data: { |
|
147 |
+ "bookId": this.selectedBookId |
|
148 |
+ }, |
|
149 |
+ }) |
|
150 |
+ .then(response => { |
|
151 |
+ this.units = response.data; |
|
152 |
+ this.selectedTextId = ''; |
|
153 |
+ }) |
|
154 |
+ .catch(error => { |
|
155 |
+ console.error("fetchUnits - error: ", error); |
|
156 |
+ alert("단원 목록을 불러오는 중 오류가 발생했습니다."); |
|
157 |
+ }); |
|
158 |
+ }, |
|
83 | 159 |
|
84 | 160 |
// 책과 단원 이름을 가져오는 메서드 |
85 | 161 |
fetchBookAndUnitNames() { |
... | ... | @@ -87,6 +163,7 @@ |
87 | 163 |
axios.post('/book/findAll.json') |
88 | 164 |
.then(response => { |
89 | 165 |
const book = response.data.find(book => book.book_id === this.selectedBookId); |
166 |
+ this.books = response.data; |
|
90 | 167 |
if (book) { |
91 | 168 |
this.bookName = book.book_nm; |
92 | 169 |
this.updateTitleMessage(); // 책 이름을 가져온 후에 제목 업데이트 |
... | ... | @@ -100,6 +177,8 @@ |
100 | 177 |
axios.post('/unit/unitList.json', { bookId: this.selectedBookId }) |
101 | 178 |
.then(response => { |
102 | 179 |
const unit = response.data.find(unit => unit.unitId === this.selectedUnitId); |
180 |
+ this.units = response.data; |
|
181 |
+ |
|
103 | 182 |
if (unit) { |
104 | 183 |
this.unitName = unit.unitName; |
105 | 184 |
this.updateTitleMessage(); // 단원 이름을 가져온 후에 제목 업데이트 |
... | ... | @@ -117,23 +196,33 @@ |
117 | 196 |
|
118 | 197 |
// 지문 목록을 가져오는 메서드 |
119 | 198 |
fetchTexts() { |
120 |
- axios.post('/text/selectTextList.json', { |
|
121 |
- page: 1, |
|
122 |
- pageSize: 100 |
|
199 |
+ axios({ |
|
200 |
+ url: "/text/textSearch.json", |
|
201 |
+ method: "post", |
|
202 |
+ headers: { |
|
203 |
+ "Content-Type": "application/json; charset=UTF-8", |
|
204 |
+ }, |
|
205 |
+ data: { |
|
206 |
+ "unitId": this.selectedUnitId |
|
207 |
+ }, |
|
123 | 208 |
}) |
124 |
- .then(response => { |
|
125 |
- this.texts = response.data.texts; |
|
126 |
- }) |
|
127 |
- .catch(error => { |
|
128 |
- console.error("지문 목록 가져오기 실패: ", error); |
|
129 |
- }); |
|
209 |
+ .then(response => { |
|
210 |
+ this.texts = response.data.list; |
|
211 |
+ }) |
|
212 |
+ .catch(error => { |
|
213 |
+ console.error("fetchTexts - error: ", error); |
|
214 |
+ alert("지문 목록을 불러오는 중 오류가 발생했습니다."); |
|
215 |
+ }); |
|
130 | 216 |
}, |
131 | 217 |
|
132 | 218 |
addWord() { // 단어 추가 |
133 | 219 |
if (this.newWord.eng && this.newWord.kor) { |
134 |
- this.words.push({ ...this.newWord }); |
|
220 |
+ const newWordWithFile = { ...this.newWord }; // 새 단어 객체 |
|
221 |
+ this.words.push(newWordWithFile); |
|
222 |
+ // 초기화 |
|
135 | 223 |
this.newWord.eng = ''; |
136 | 224 |
this.newWord.kor = ''; |
225 |
+ this.newWord.fileMngId = null; // 파일 매니지 ID 초기화 |
|
137 | 226 |
} else { |
138 | 227 |
console.log("단어 입력이 비어 있음"); |
139 | 228 |
} |
... | ... | @@ -162,6 +251,32 @@ |
162 | 251 |
return []; |
163 | 252 |
}); |
164 | 253 |
}, |
254 |
+ |
|
255 |
+ // 파일 업로드 메서드 |
|
256 |
+ async uploadFiles(files) { |
|
257 |
+ if (!files || files.length === 0) { |
|
258 |
+ return null; |
|
259 |
+ } |
|
260 |
+ |
|
261 |
+ const formData = new FormData(); |
|
262 |
+ for (let i = 0; i < files.length; i++) { |
|
263 |
+ formData.append("files", files[i]); |
|
264 |
+ } |
|
265 |
+ |
|
266 |
+ try { |
|
267 |
+ const response = await axios.post("/file/upload.json", formData, { |
|
268 |
+ headers: { |
|
269 |
+ "Content-Type": "multipart/form-data", |
|
270 |
+ }, |
|
271 |
+ }); |
|
272 |
+ |
|
273 |
+ return response.data.fileMngId; // 파일 매니지 ID 반환 |
|
274 |
+ } catch (error) { |
|
275 |
+ console.error("파일 업로드 오류:", error); |
|
276 |
+ throw new Error("파일 업로드에 실패했습니다."); |
|
277 |
+ } |
|
278 |
+ }, |
|
279 |
+ |
|
165 | 280 |
|
166 | 281 |
async registerWordBook() { |
167 | 282 |
const vm = this; |
... | ... | @@ -208,7 +323,7 @@ |
208 | 323 |
wdBookId: wdBookId, |
209 | 324 |
wdNm: word.eng, |
210 | 325 |
wdMnng: word.kor, |
211 |
- fileMngId: '1' |
|
326 |
+ fileMngId: word.fileMngId, |
|
212 | 327 |
}); |
213 | 328 |
} |
214 | 329 |
|
... | ... | @@ -218,7 +333,7 @@ |
218 | 333 |
wdBookId: wdBookId, |
219 | 334 |
wdNm: word.eng, |
220 | 335 |
wdMnng: word.kor, |
221 |
- fileMngId: '1' |
|
336 |
+ fileMngId: word.fileMngId, |
|
222 | 337 |
}); |
223 | 338 |
} |
224 | 339 |
|
... | ... | @@ -235,7 +350,7 @@ |
235 | 350 |
|
236 | 351 |
alert('단어장이 성공적으로 등록되었습니다.'); |
237 | 352 |
vm.goToPage('VocaList'); |
238 |
- |
|
353 |
+ |
|
239 | 354 |
} catch (error) { |
240 | 355 |
console.error('단어장 등록 중 오류 발생:', error); |
241 | 356 |
alert('단어장 등록에 실패했습니다.'); |
--- client/views/pages/teacher/VocaList.vue
+++ client/views/pages/teacher/VocaList.vue
... | ... | @@ -35,7 +35,7 @@ |
35 | 35 |
<td>작성자</td> |
36 | 36 |
</thead> |
37 | 37 |
<tbody> |
38 |
- <tr v-for="(wordBook, index) in dataList" :key="wordBook.wdBookId" @click="goToViewPage('VocaDetail')"> |
|
38 |
+ <tr v-for="(wordBook, index) in dataList" :key="wordBook.wdBookId" @click="goToViewPage('VocaDetail', wordBook.wdBookId)"> |
|
39 | 39 |
<td>{{ createNo(index) }}</td> |
40 | 40 |
<td>{{ wordBook.textTtl }}</td> |
41 | 41 |
<td>{{ wordBook.wordsPreview }}</td> |
... | ... | @@ -209,12 +209,13 @@ |
209 | 209 |
}, |
210 | 210 |
|
211 | 211 |
// 페이지 이동 메서드 |
212 |
- goToViewPage(page) { |
|
212 |
+ goToViewPage(page, wordbookId) { |
|
213 | 213 |
this.$router.push({ |
214 | 214 |
name: page, |
215 | 215 |
query: { |
216 | 216 |
selectedBookId: this.selectedBookId, |
217 |
- selectedUnitId: this.selectedUnitId |
|
217 |
+ selectedUnitId: this.selectedUnitId, |
|
218 |
+ wdBookId: wordbookId |
|
218 | 219 |
} |
219 | 220 |
}); |
220 | 221 |
}, |
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?