
--- client/resources/api/category.js
+++ client/resources/api/category.js
... | ... | @@ -18,4 +18,4 @@ |
18 | 18 |
// 카테고리 정보 등록 |
19 | 19 |
export const saveCategory = insertCtgryDTO => { |
20 | 20 |
return apiClient.post(`/category/saveCategory.json`, insertCtgryDTO); |
21 |
-} |
|
21 |
+}(파일 끝에 줄바꿈 문자 없음) |
--- client/resources/api/dcry.js
+++ client/resources/api/dcry.js
... | ... | @@ -1,14 +1,26 @@ |
1 |
-import {apiClient} from "./index"; |
|
1 |
+import { apiClient, fileClient } from "./index"; |
|
2 | 2 |
|
3 | 3 |
// 기록물 목록 조회 |
4 | 4 |
export const findAllDatas = (searchReqDTO) => { |
5 |
- return apiClient.get(`/main/findAllDatas.json`, { params: searchReqDTO }); |
|
5 |
+ return apiClient.get(`/dcry/findAllDatas.json`, { params: searchReqDTO }); |
|
6 | 6 |
} |
7 | 7 |
|
8 | 8 |
// 기록물 상세 조회 |
9 |
+export const findDcryProc = (dcryId) => { |
|
10 |
+ return apiClient.get(`/dcry/${dcryId}/findDcry.json`); |
|
11 |
+} |
|
9 | 12 |
|
10 | 13 |
// 기록물 등록 |
14 |
+export const saveDcry = (formData) => { |
|
15 |
+ return fileClient.post(`/dcry/saveDcry.file`, formData); |
|
16 |
+} |
|
11 | 17 |
|
12 | 18 |
// 기록물 수정 |
19 |
+export const updateDcry = (formData) => { |
|
20 |
+ return fileClient.put(`/dcry/updateDcry.file`, formData); |
|
21 |
+} |
|
13 | 22 |
|
14 |
-// 기록물 삭제(파일 끝에 줄바꿈 문자 없음) |
|
23 |
+// 기록물 삭제 |
|
24 |
+export const deleteDcryProc = (dcryId) => { |
|
25 |
+ return apiClient.put(`/dcry/${dcryId}/deleteDcry.json`); |
|
26 |
+}(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/api/file.js
... | ... | @@ -0,0 +1,11 @@ |
1 | +import { apiClient } from "./index"; | |
2 | + | |
3 | +// 파일 다운로드 | |
4 | +export const fileDownloadProc = (fileId) => { | |
5 | + return apiClient.get(`/file/${fileId}/fileDownload.json`, { responseType: 'blob' }); | |
6 | +} | |
7 | + | |
8 | +// 파일 다운로드 | |
9 | +export const multiFileDownloadProc = (fileIds) => { | |
10 | + return apiClient.get(`/file/multiFileDownload.json`, { responseType: 'blob', params: { fileIds } }); | |
11 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/js/cmmnPlugin.js
... | ... | @@ -0,0 +1,26 @@ |
1 | +export default { | |
2 | + install(Vue) { | |
3 | + // 빈값체크 | |
4 | + Vue.config.globalProperties.$isEmpty = function (data) { | |
5 | + if (data === undefined || data === null || data === "" || data.length === 0 || (data.constructor == Object && Object.keys(data).length === 0)) { | |
6 | + if ((typeof data) === "number") { | |
7 | + return false | |
8 | + } else { | |
9 | + return true; | |
10 | + } | |
11 | + } else { | |
12 | + return false; | |
13 | + } | |
14 | + } | |
15 | + | |
16 | + /** | |
17 | + * FROM: 2025-03-25 13:32 | |
18 | + * TO : 2025.03.25 | |
19 | + */ | |
20 | + Vue.config.globalProperties.$dotFormatDate = (dateString) => { | |
21 | + if (!dateString) return ''; | |
22 | + const dateOnly = dateString.split(' ')[0]; | |
23 | + return dateOnly.replace(/-/g, '.'); | |
24 | + } | |
25 | + } | |
26 | +}(파일 끝에 줄바꿈 문자 없음) |
--- client/views/component/EditorComponent.vue
+++ client/views/component/EditorComponent.vue
... | ... | @@ -6,9 +6,7 @@ |
6 | 6 |
* This configuration was generated using the CKEditor 5 Builder. You can modify it anytime using this link: |
7 | 7 |
* https://ckeditor.com/ckeditor-5/builder/#installation/NoNgNARATAdAzPCkCMAGKIAcmCsrdwghzICcUqqyyALESDnKXFKW6djTdkhANYB7JKjDBkYEZIlhkAXUggAZmxwBjVRFlA== |
8 | 8 |
*/ |
9 |
- |
|
10 | 9 |
import { Ckeditor } from '@ckeditor/ckeditor5-vue'; |
11 |
- |
|
12 | 10 |
import { |
13 | 11 |
ClassicEditor, |
14 | 12 |
Alignment, |
... | ... | @@ -21,8 +19,6 @@ |
21 | 19 |
FontFamily, |
22 | 20 |
FontSize, |
23 | 21 |
HorizontalLine, |
24 |
- ImageEditing, |
|
25 |
- ImageUtils, |
|
26 | 22 |
Indent, |
27 | 23 |
IndentBlock, |
28 | 24 |
Italic, |
... | ... | @@ -41,16 +37,12 @@ |
41 | 37 |
TextPartLanguage, |
42 | 38 |
Underline, |
43 | 39 |
} from 'ckeditor5'; |
44 |
- |
|
45 | 40 |
import translations from 'ckeditor5/translations/ko.js'; |
46 |
- |
|
47 | 41 |
import 'ckeditor5/ckeditor5.css'; |
48 |
- |
|
49 | 42 |
/** |
50 | 43 |
* Create a free account with a trial: https://portal.ckeditor.com/checkout?plan=free |
51 | 44 |
*/ |
52 | 45 |
const LICENSE_KEY = 'GPL'; // or <YOUR_LICENSE_KEY>. |
53 |
- |
|
54 | 46 |
export default { |
55 | 47 |
name: 'EditorComponent', |
56 | 48 |
components: { |
... | ... | @@ -78,7 +70,6 @@ |
78 | 70 |
if (!this.isLayoutReady) { |
79 | 71 |
return null; |
80 | 72 |
} |
81 |
- |
|
82 | 73 |
return { |
83 | 74 |
toolbar: { |
84 | 75 |
items: [ |
... | ... | @@ -118,8 +109,6 @@ |
118 | 109 |
FontFamily, |
119 | 110 |
FontSize, |
120 | 111 |
HorizontalLine, |
121 |
- ImageEditing, |
|
122 |
- ImageUtils, |
|
123 | 112 |
Indent, |
124 | 113 |
IndentBlock, |
125 | 114 |
Italic, |
... | ... | @@ -164,7 +153,6 @@ |
164 | 153 |
}, |
165 | 154 |
methods: { |
166 | 155 |
updateContents(data) { |
167 |
- // 에디터 내용이 변경될 때 부모 컴포넌트에 알림 |
|
168 | 156 |
this.$emit('update:contents', data); |
169 | 157 |
} |
170 | 158 |
} |
--- client/views/component/List/CardViewList.vue
+++ client/views/component/List/CardViewList.vue
... | ... | @@ -24,7 +24,7 @@ |
24 | 24 |
</div> |
25 | 25 |
<div class="date"> |
26 | 26 |
<ul> |
27 |
- <li>생산연도 <b>{{ item.prdctnYear }}</b></li> |
|
27 |
+ <li>생산연도 <b>{{ item.prdctnYear ? item.prdctnYear : '-' }}</b></li> |
|
28 | 28 |
<li>|</li> |
29 | 29 |
<li>등록일 <b>{{ item.rgsde }}</b></li> |
30 | 30 |
</ul> |
+++ client/views/component/ViewerComponent.vue
... | ... | @@ -0,0 +1,15 @@ |
1 | +<template> | |
2 | + <div class="ck-content" v-html="content"></div> | |
3 | +</template> | |
4 | +<script> | |
5 | +import 'ckeditor5/ckeditor5.css'; | |
6 | +export default { | |
7 | + name: 'ViewerComponent', | |
8 | + props: { | |
9 | + content: { | |
10 | + type: String, | |
11 | + default: '' | |
12 | + } | |
13 | + } | |
14 | +}; | |
15 | +</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/component/modal/CategorySelectModal.vue
+++ client/views/component/modal/CategorySelectModal.vue
... | ... | @@ -6,8 +6,9 @@ |
6 | 6 |
<button @click="closeModal" class="closebtn">✕</button> |
7 | 7 |
</div> |
8 | 8 |
<div class="modal-search flex-center mb-20"> |
9 |
- <input type="text" placeholder="카테고리명을 입력하세요."> |
|
10 |
- <button class="search-btn"><img :src="searchicon" alt=""> |
|
9 |
+ <input type="text" placeholder="카테고리명을 입력하세요." v-model="searchReqDTO.searchText" @keyup.enter="fnFindAllCategory"> |
|
10 |
+ <button type="button" class="search-btn" @click="fnFindAllCategory"> |
|
11 |
+ <img :src="searchicon" alt=""> |
|
11 | 12 |
<p>검색</p> |
12 | 13 |
</button> |
13 | 14 |
</div> |
... | ... | @@ -19,27 +20,28 @@ |
19 | 20 |
</tr> |
20 | 21 |
</thead> |
21 | 22 |
<tbody> |
22 |
- <tr v-for="item in items" :key="item.id"> |
|
23 |
- <!-- Category 칼럼 --> |
|
24 |
- <td> {{ item.category }} </td> |
|
25 |
- <!-- Checkbox 칼럼 --> |
|
26 |
- <td> |
|
27 |
- <input type="checkbox" v-model="item.selected" /> |
|
28 |
- </td> |
|
23 |
+ <template v-if="list.length > 0"> |
|
24 |
+ <tr v-for="(item, idx) of list" :key="idx" :class="{ 'selected': item.isSelected }"> |
|
25 |
+ <td>{{ item.ctgryNm }}</td> |
|
26 |
+ <td><input type="checkbox" :value="item" v-model="selectedList" /></td> |
|
27 |
+ </tr> |
|
28 |
+ </template> |
|
29 |
+ <tr v-else> |
|
30 |
+ <td colspan="2">조건에 맞는 카테고리가 존재하지 않습니다.</td> |
|
29 | 31 |
</tr> |
30 | 32 |
</tbody> |
31 | 33 |
</table> |
32 |
- <div class="flex-end mb-30"><button class="register-b " @click="registerCategories">등록</button></div> |
|
34 |
+ <div class="flex-end mb-30"><button class="register-b" @click="fnAddCtgries">등록</button></div> |
|
33 | 35 |
<div class="pagination"> |
34 | 36 |
<!-- Previous and Next Page Buttons --> |
35 | 37 |
<button> |
36 | 38 |
<DoubleLeftOutlined /> |
37 | 39 |
</button> |
38 |
- <button @click="previousPage" :disabled="currentPage === 1"> |
|
40 |
+ <button @click="previousPage" :disabled="searchReqDTO.currentPage === 1"> |
|
39 | 41 |
<LeftOutlined /> |
40 | 42 |
</button> |
41 | 43 |
<button class="page-number clicked">1</button> |
42 |
- <button @click="nextPage" :disabled="currentPage === totalPages"> |
|
44 |
+ <button @click="nextPage" :disabled="searchReqDTO.currentPage === searchReqDTO.recordSize"> |
|
43 | 45 |
<RightOutlined /> |
44 | 46 |
</button> |
45 | 47 |
<button> |
... | ... | @@ -51,59 +53,109 @@ |
51 | 53 |
</template> |
52 | 54 |
<script> |
53 | 55 |
import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'; |
56 |
+// API |
|
57 |
+import { findAllCategoryProc } from '@/resources/api/category'; |
|
54 | 58 |
|
55 | 59 |
export default { |
56 | 60 |
name: 'CategorySelectModal', |
61 |
+ |
|
57 | 62 |
components: { |
58 | 63 |
DoubleLeftOutlined, |
59 | 64 |
LeftOutlined, |
60 | 65 |
RightOutlined, |
61 | 66 |
DoubleRightOutlined |
62 | 67 |
}, |
68 |
+ |
|
63 | 69 |
props: { |
64 | 70 |
selectedCtgries: { |
65 | 71 |
type: Array, |
66 | 72 |
default: () => [], |
67 | 73 |
} |
68 | 74 |
}, |
75 |
+ |
|
69 | 76 |
data() { |
70 | 77 |
return { |
71 |
- items: [ |
|
72 |
- { id: 1, category: '카테고리 1', selected: false }, |
|
73 |
- { id: 2, category: '카테고리 2', selected: false }, |
|
74 |
- { id: 3, category: '카테고리 3', selected: false }, |
|
75 |
- ], |
|
78 |
+ // ICON |
|
76 | 79 |
searchicon: 'client/resources/images/icon/search.png', |
77 |
- currentPage: 1, |
|
78 |
- totalPages: 1 |
|
80 |
+ |
|
81 |
+ // 검색 객체 |
|
82 |
+ searchReqDTO: { |
|
83 |
+ currentPage: 1, |
|
84 |
+ recordSize: 10, |
|
85 |
+ pageSize: 10, |
|
86 |
+ totalRecordCount: 5, |
|
87 |
+ totalPageCount: 1, |
|
88 |
+ startPage: 1, |
|
89 |
+ endPage: 1, |
|
90 |
+ limitStart: 0, |
|
91 |
+ existPrevPage: false, |
|
92 |
+ existNextPage: false, |
|
93 |
+ searchType: 'nm', |
|
94 |
+ searchText: null, |
|
95 |
+ useAt: 'Y', |
|
96 |
+ selectedCtgryIds: null, |
|
97 |
+ }, |
|
98 |
+ |
|
99 |
+ list: [], // 카테고리 목록 |
|
100 |
+ selectedList: [] // 선택한 카테고리 목록 |
|
79 | 101 |
}; |
80 | 102 |
}, |
103 |
+ |
|
104 |
+ created() { |
|
105 |
+ this.fnFindAllCategory(); // 목록 조회 |
|
106 |
+ }, |
|
107 |
+ |
|
81 | 108 |
methods: { |
109 |
+ // 목록 조회 |
|
110 |
+ async fnFindAllCategory() { |
|
111 |
+ try { |
|
112 |
+ if (this.selectedCtgries.length > 0) { |
|
113 |
+ this.searchReqDTO.selectedCtgryIds = this.selectedCtgries.map(item => item.ctgryId).join(','); |
|
114 |
+ } |
|
115 |
+ console.log('req: ', this.searchReqDTO); |
|
116 |
+ |
|
117 |
+ const response = await findAllCategoryProc(this.searchReqDTO); |
|
118 |
+ console.log('res: ', response.data.data.ctgry); |
|
119 |
+ |
|
120 |
+ let ctgries = response.data.data.ctgry; |
|
121 |
+ for (let item of ctgries) { |
|
122 |
+ item.isSelected = false; |
|
123 |
+ for (let ctgry of this.selectedCtgries) { |
|
124 |
+ if (item.ctgryId === ctgry.ctgryId) { |
|
125 |
+ item.isSelected = true; |
|
126 |
+ break; |
|
127 |
+ } |
|
128 |
+ } |
|
129 |
+ } |
|
130 |
+ |
|
131 |
+ this.list = ctgries; |
|
132 |
+ } catch (error) { |
|
133 |
+ alert('조회중 오류가 발생했습니다.'); |
|
134 |
+ |
|
135 |
+ if (error.response) { |
|
136 |
+ alert(error.response.data.message); |
|
137 |
+ } |
|
138 |
+ console.error(error.message); |
|
139 |
+ } |
|
140 |
+ }, |
|
141 |
+ |
|
82 | 142 |
closeModal() { |
83 | 143 |
this.$emit('toggleModal'); |
84 | 144 |
}, |
85 |
- registerCategories() { |
|
86 |
- // 선택된 카테고리 목록 |
|
87 |
- const selectedCategories = this.items |
|
88 |
- .filter(item => item.selected) |
|
89 |
- .map(item => item.category); |
|
90 | 145 |
|
91 |
- // 선택된 카테고리 ID 목록 |
|
92 |
- const selectedIds = this.items |
|
93 |
- .filter(item => item.selected) |
|
94 |
- .map(item => item.id); |
|
95 |
- |
|
96 |
- // 부모 컴포넌트로 전달하고 모달 닫기 |
|
97 |
- this.$emit('toggleModal', selectedIds); |
|
146 |
+ fnAddCtgries() { |
|
147 |
+ this.$emit('addCtgries', this.selectedList); |
|
98 | 148 |
}, |
149 |
+ |
|
99 | 150 |
previousPage() { |
100 |
- if (this.currentPage > 1) { |
|
101 |
- this.currentPage--; |
|
151 |
+ if (this.searchReqDTO.currentPage > 1) { |
|
152 |
+ this.searchReqDTO.currentPage--; |
|
102 | 153 |
} |
103 | 154 |
}, |
155 |
+ |
|
104 | 156 |
nextPage() { |
105 |
- if (this.currentPage < this.totalPages) { |
|
106 |
- this.currentPage++; |
|
157 |
+ if (this.searchReqDTO.currentPage < this.searchReqDTO.recordSize) { |
|
158 |
+ this.searchReqDTO.currentPage++; |
|
107 | 159 |
} |
108 | 160 |
} |
109 | 161 |
} |
--- client/views/index.js
+++ client/views/index.js
... | ... | @@ -3,11 +3,13 @@ |
3 | 3 |
import App from "./App.vue"; |
4 | 4 |
import Store from "./pages/AppStore.js"; |
5 | 5 |
import Router from "./pages/AppRouter.js"; |
6 |
+import cmmnPlugin from '@/resources/js/cmmnPlugin.js' |
|
6 | 7 |
|
7 | 8 |
async function initVueApp() { |
8 | 9 |
const vue = createApp(App) |
9 | 10 |
.use(Router) |
10 | 11 |
.use(Store) |
12 |
+ .use(cmmnPlugin) |
|
11 | 13 |
vue.config.devtools = true; |
12 | 14 |
vue.mount("#root"); |
13 | 15 |
} |
--- client/views/pages/user/PicHistoryDetail.vue
+++ client/views/pages/user/PicHistoryDetail.vue
... | ... | @@ -1,171 +1,253 @@ |
1 | 1 |
<template> |
2 |
- <div class="content"> |
|
3 |
- <div class="sub-title-area mb-30"> |
|
4 |
- <h2>사진 기록물</h2> |
|
5 |
- <div class="breadcrumb-list"> |
|
6 |
- <ul> |
|
7 |
- <li><img :src="homeicon" alt="Home Icon"> |
|
8 |
- <p>기록물</p> |
|
9 |
- </li> |
|
10 |
- <li><img :src="righticon" alt=""></li> |
|
11 |
- <li>사진 기록물</li> |
|
12 |
- </ul> |
|
13 |
- </div> |
|
14 |
- </div> |
|
15 |
- <form action="" class="gallery-form mb-40"> |
|
16 |
- <dl class="mb-20"> |
|
17 |
- <dd> |
|
18 |
- <p>사진 기록물 제목1 |
|
19 |
- </p> |
|
20 |
- <div class="date flex align-center"> |
|
21 |
- <img :src="calendaricon" alt=""> |
|
22 |
- <span>2025.02.28</span> |
|
23 |
- </div> |
|
24 |
- </dd> |
|
25 |
- |
|
26 |
- </dl> |
|
27 |
- <div> |
|
28 |
- <div class="gallery"> |
|
29 |
- <div class="main-swiper"> |
|
30 |
- <swiper :style="{ |
|
31 |
- '--swiper-navigation-color': '#fff', |
|
32 |
- '--swiper-pagination-color': '#fff', |
|
33 |
- }" :loop="true" :spaceBetween="10" :thumbs="{ swiper: thumbsSwiper }" :modules="modules" |
|
34 |
- class="mySwiper2"> |
|
35 |
- <swiper-slide v-for="(item, index) in slides" :key="index"> |
|
36 |
- <img :src="item.img" :alt="item.alt" /> |
|
37 |
- </swiper-slide> |
|
38 |
- </swiper> |
|
39 |
- </div> |
|
40 |
- <div class="thumbnail"> |
|
41 |
- <swiper @swiper="setThumbsSwiper" :spaceBetween="20" :slidesPerView="4" :freeMode="true" |
|
42 |
- :watchSlidesProgress="true" :modules="modules" :navigation="true" class="mySwiper"> |
|
43 |
- <swiper-slide v-for="(item, index) in slides" :key="index"> |
|
44 |
- <img :src="item.img" :alt="item.alt" /> |
|
45 |
- </swiper-slide> |
|
46 |
- </swiper> |
|
47 |
- </div> |
|
48 |
- </div> |
|
49 |
- <div class="btn-group"> |
|
50 |
- <button class="select-down">선택 다운로드</button> |
|
51 |
- <button class="all-down">전체 다운로드</button> |
|
52 |
- </div> |
|
53 |
- </div> |
|
54 |
- </form> |
|
55 |
- |
|
56 |
- <h3>내용</h3> |
|
57 |
- <form action="" class=" info-form mb-50"> |
|
58 |
- <dl> |
|
59 |
- <dd> |
|
60 |
- <p> 대한민국 최대의 내륙 산업단지를 보유하고, 서울로부터 277km, 부산으로부터 167km 거리에 있으며, 면적은 615㎢로 경상북도 전체 면적의 3.2%에 달합니다. 인구는 |
|
61 |
- 41만 명이고, 선산읍, 고아읍, 산동읍을 비롯한 3읍, 5면, 17개 동으로 구성되어 있습니다.</p> |
|
62 |
- |
|
63 |
- </dd> |
|
64 |
- </dl> |
|
65 |
- </form> |
|
66 |
- |
|
67 |
- <h3>기본정보</h3> |
|
68 |
- <form action="" class="info-form mb-50"> |
|
69 |
- <dl> |
|
70 |
- <dd class="mb-20"> |
|
71 |
- <img :src="addressicon" alt=""> |
|
72 |
- <span>주소</span> |
|
73 |
- <p>경상북도 구미시 송정대로 55</p> |
|
74 |
- </dd> |
|
75 |
- <dd class="mb-20"> |
|
76 |
- <img :src="yearicon" alt=""> |
|
77 |
- <span>생산연도</span> |
|
78 |
- <p>2017</p> |
|
79 |
- |
|
80 |
- </dd> |
|
81 |
- <dd> |
|
82 |
- <img :src="categoryicon" alt=""> |
|
83 |
- <span>카테고리</span> |
|
84 |
- <ul class="category"> |
|
85 |
- <li v-if="resultitem.category1" class="category1">카테고리1</li> |
|
86 |
- <li v-if="resultitem.category2" class="category2">카테고리2</li> |
|
87 |
- </ul> |
|
88 |
- |
|
89 |
- </dd> |
|
90 |
- |
|
91 |
- </dl> |
|
92 |
- </form> |
|
93 |
- <div class="btn-group flex-center"> |
|
94 |
- <button class="red-line " type="button" @click="fnDeleteUser">삭제</button> |
|
95 |
- <button class="blue-line " type="button" @click="fnUpdateUser">수정</button> |
|
96 |
- <button class="gray-line-bg " type="button" @click="fnUpdateUser">목록</button> |
|
97 |
- </div> |
|
2 |
+ <div class="content"> |
|
3 |
+ <div class="sub-title-area mb-30"> |
|
4 |
+ <h2>사진 기록물</h2> |
|
5 |
+ <div class="breadcrumb-list"> |
|
6 |
+ <ul> |
|
7 |
+ <li><img :src="homeicon" alt="Home Icon"> |
|
8 |
+ <p>기록물</p> |
|
9 |
+ </li> |
|
10 |
+ <li><img :src="righticon" alt=""></li> |
|
11 |
+ <li>사진 기록물</li> |
|
12 |
+ </ul> |
|
13 |
+ </div> |
|
98 | 14 |
</div> |
15 |
+ <div class="gallery-form mb-40"> |
|
16 |
+ <dl class="mb-20"> |
|
17 |
+ <dd> |
|
18 |
+ <p>{{ dcry.sj }}</p> |
|
19 |
+ <div class="date flex align-center"> |
|
20 |
+ <img :src="calendaricon" alt=""> |
|
21 |
+ <span>{{ $dotFormatDate(dcry.rgsde) }}</span> |
|
22 |
+ </div> |
|
23 |
+ </dd> |
|
24 |
+ </dl> |
|
25 |
+ <div> |
|
26 |
+ <div class="gallery"> |
|
27 |
+ <div class="main-swiper"> |
|
28 |
+ <swiper :style="{ '--swiper-navigation-color': '#fff', '--swiper-pagination-color': '#fff' }" :loop="true" :spaceBetween="10" :thumbs="{ swiper: thumbsSwiper }" :modules="modules" class="mySwiper2"> |
|
29 |
+ <swiper-slide v-for="(item, idx) of dcry.files" :key="idx"> |
|
30 |
+ <img :src="item.filePath" :alt="item.fileNm" /> |
|
31 |
+ </swiper-slide> |
|
32 |
+ </swiper> |
|
33 |
+ </div> |
|
34 |
+ <div class="thumbnail"> |
|
35 |
+ <swiper @swiper="setThumbsSwiper" :spaceBetween="20" :slidesPerView="4" :freeMode="true" :watchSlidesProgress="true" :modules="modules" :navigation="true" class="mySwiper"> |
|
36 |
+ <swiper-slide v-for="(item, idx) of dcry.files" :key="idx"> |
|
37 |
+ <input type="checkbox" :id="'photo_' + idx" :value="item.fileId" v-model="selectedFiles" /> |
|
38 |
+ <img :src="item.filePath" :alt="item.fileNm" /> |
|
39 |
+ </swiper-slide> |
|
40 |
+ </swiper> |
|
41 |
+ </div> |
|
42 |
+ </div> |
|
43 |
+ <div class="btn-group"> |
|
44 |
+ <button class="select-down" @click="fnDownload('selected')">선택 다운로드</button> |
|
45 |
+ <button class="all-down" @click="fnDownload('all')">전체 다운로드</button> |
|
46 |
+ </div> |
|
47 |
+ </div> |
|
48 |
+ </div> |
|
49 |
+ <h3>내용</h3> |
|
50 |
+ <div class="info-form mb-50"> |
|
51 |
+ <dl> |
|
52 |
+ <dd> |
|
53 |
+ <ViewerComponent :content="dcry.cn" /> |
|
54 |
+ </dd> |
|
55 |
+ </dl> |
|
56 |
+ </div> |
|
57 |
+ <h3>기본정보</h3> |
|
58 |
+ <div class="info-form mb-50"> |
|
59 |
+ <dl> |
|
60 |
+ <dd class="mb-20"> |
|
61 |
+ <img :src="addressicon" alt=""> |
|
62 |
+ <span>주소</span> |
|
63 |
+ <p>{{ dcry.adres }}</p> |
|
64 |
+ </dd> |
|
65 |
+ <dd class="mb-20"> |
|
66 |
+ <img :src="yearicon" alt=""> |
|
67 |
+ <span>생산연도</span> |
|
68 |
+ <p>{{ $dotFormatDate(dcry.prdctnYear) }}</p> |
|
69 |
+ </dd> |
|
70 |
+ <dd> |
|
71 |
+ <img :src="categoryicon" alt=""> |
|
72 |
+ <span>카테고리</span> |
|
73 |
+ <ul class="category"> |
|
74 |
+ <li v-for="(item, idx) of dcry.ctgrys" :key="idx" class="category">{{ item.ctgryNm }}</li> |
|
75 |
+ </ul> |
|
76 |
+ </dd> |
|
77 |
+ </dl> |
|
78 |
+ </div> |
|
79 |
+ <div class="btn-group flex-center"> |
|
80 |
+ <button class="red-line " type="button" @click="fnDelete">삭제</button> |
|
81 |
+ <button class="blue-line " type="button" @click="fnMoveTo('PicHistoryInsert', pageId)">수정</button> |
|
82 |
+ <button class="gray-line-bg " type="button" @click="fnMoveTo('PicHistorySearch')">목록</button> |
|
83 |
+ </div> |
|
84 |
+ </div> |
|
99 | 85 |
</template> |
100 |
- |
|
101 | 86 |
<script> |
102 |
-import axios from "axios"; |
|
103 | 87 |
import { ref } from 'vue'; |
104 |
-import { updateUsers, logOutProc, updatePassword } from "../../../resources/api/user" |
|
105 | 88 |
// Import Swiper Vue components |
106 | 89 |
import { CaretRightOutlined, PauseOutlined } from '@ant-design/icons-vue'; |
107 | 90 |
import { Swiper, SwiperSlide } from 'swiper/vue'; |
108 |
- |
|
109 | 91 |
// Import Swiper styles |
110 | 92 |
import 'swiper/css'; |
111 |
- |
|
112 | 93 |
import 'swiper/css/free-mode'; |
113 | 94 |
import 'swiper/css/navigation'; |
114 | 95 |
import 'swiper/css/thumbs'; |
115 |
- |
|
116 | 96 |
// import required modules |
117 | 97 |
import { FreeMode, Navigation, Thumbs } from 'swiper/modules'; |
98 |
+// COMPONENT |
|
99 |
+import ViewerComponent from '../../component/ViewerComponent.vue'; |
|
100 |
+// API |
|
101 |
+import { findDcryProc, deleteDcryProc } from '@/resources/api/dcry'; |
|
102 |
+import { fileDownloadProc, multiFileDownloadProc } from '@/resources/api/file'; |
|
118 | 103 |
|
119 | 104 |
export default { |
120 |
- components: { |
|
121 |
- PauseOutlined, |
|
122 |
- CaretRightOutlined, |
|
123 |
- Swiper, |
|
124 |
- SwiperSlide, |
|
125 |
- }, |
|
126 |
- setup() { |
|
127 |
- const thumbsSwiper = ref(null); |
|
105 |
+ components: { |
|
106 |
+ PauseOutlined, |
|
107 |
+ CaretRightOutlined, |
|
108 |
+ Swiper, |
|
109 |
+ SwiperSlide, |
|
110 |
+ ViewerComponent, |
|
111 |
+ }, |
|
112 |
+ setup() { |
|
113 |
+ const thumbsSwiper = ref(null); |
|
128 | 114 |
|
129 |
- const setThumbsSwiper = (swiper) => { |
|
130 |
- thumbsSwiper.value = swiper; |
|
131 |
- }; |
|
115 |
+ const setThumbsSwiper = (swiper) => { |
|
116 |
+ thumbsSwiper.value = swiper; |
|
117 |
+ }; |
|
132 | 118 |
|
133 |
- return { |
|
134 |
- thumbsSwiper, |
|
135 |
- setThumbsSwiper, |
|
136 |
- modules: [FreeMode, Navigation, Thumbs], |
|
137 |
- }; |
|
138 |
- }, |
|
139 |
- data() { |
|
140 |
- return { |
|
141 |
- resultitem: { |
|
142 |
- category1: true, |
|
143 |
- category2: true, |
|
144 |
- }, |
|
145 |
- calendaricon: 'client/resources/images/icon/calendaricon.png', |
|
146 |
- homeicon: 'client/resources/images/icon/home.png', |
|
147 |
- erroricon: 'client/resources/images/icon/error.png', |
|
148 |
- righticon: 'client/resources/images/icon/right.png', |
|
149 |
- addressicon: 'client/resources/images/icon/addressicon.png', |
|
150 |
- yearicon: 'client/resources/images/icon/yearicon.png', |
|
151 |
- categoryicon: 'client/resources/images/icon/categoryicon.png', |
|
152 |
- slides: [ |
|
153 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 1' }, |
|
154 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 2' }, |
|
155 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 3' }, |
|
156 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 3' }, |
|
157 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 3' }, |
|
158 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 3' }, |
|
159 |
- // Add more slides as needed |
|
160 |
- ], |
|
119 |
+ return { |
|
120 |
+ thumbsSwiper, |
|
121 |
+ setThumbsSwiper, |
|
122 |
+ modules: [FreeMode, Navigation, Thumbs], |
|
123 |
+ }; |
|
124 |
+ }, |
|
125 |
+ data() { |
|
126 |
+ return { |
|
127 |
+ // ICON |
|
128 |
+ calendaricon: 'client/resources/images/icon/calendaricon.png', |
|
129 |
+ homeicon: 'client/resources/images/icon/home.png', |
|
130 |
+ erroricon: 'client/resources/images/icon/error.png', |
|
131 |
+ righticon: 'client/resources/images/icon/right.png', |
|
132 |
+ addressicon: 'client/resources/images/icon/addressicon.png', |
|
133 |
+ yearicon: 'client/resources/images/icon/yearicon.png', |
|
134 |
+ categoryicon: 'client/resources/images/icon/categoryicon.png', |
|
161 | 135 |
|
162 |
- }; |
|
136 |
+ pageId: null, |
|
137 |
+ dcry: {}, |
|
138 |
+ selectedFiles: [], |
|
139 |
+ }; |
|
140 |
+ }, |
|
141 |
+ methods: {}, |
|
142 |
+ watch: {}, |
|
143 |
+ computed: {}, |
|
144 |
+ created() { |
|
145 |
+ this.pageId = this.$route.query.id; |
|
146 |
+ if (this.pageId === null) { |
|
147 |
+ alert("게시물 존재하지 않습니다."); |
|
148 |
+ return; |
|
149 |
+ } |
|
150 |
+ |
|
151 |
+ this.fnFindDcry(); // 상세 조회 |
|
152 |
+ }, |
|
153 |
+ mounted() { }, |
|
154 |
+ methods: { |
|
155 |
+ // 상세 조회 |
|
156 |
+ async fnFindDcry() { |
|
157 |
+ try { |
|
158 |
+ const response = await findDcryProc(this.pageId); |
|
159 |
+ this.dcry = response.data.data.dcry; |
|
160 |
+ } catch (error) { |
|
161 |
+ alert('조회중 오류가 발생했습니다.'); |
|
162 |
+ |
|
163 |
+ if (error.response) { |
|
164 |
+ alert(error.response.data.message); |
|
165 |
+ } |
|
166 |
+ console.error(error.message); |
|
167 |
+ } |
|
163 | 168 |
}, |
164 |
- methods: { |
|
169 |
+ |
|
170 |
+ // 파일 다운로드 |
|
171 |
+ async fnDownload(type) { |
|
172 |
+ // 유효성 검사 |
|
173 |
+ if (this.selectedFiles.length === 1) { |
|
174 |
+ alert("파일을 1개 이상 선택하거나 전체 다운로드를 클릭해주세요."); |
|
175 |
+ return; |
|
176 |
+ } |
|
177 |
+ |
|
178 |
+ let url = null; |
|
179 |
+ let link = null; |
|
180 |
+ |
|
181 |
+ try { |
|
182 |
+ // 파일 ID 수집 |
|
183 |
+ let fileIds = null; |
|
184 |
+ if (type === 'selected') { |
|
185 |
+ fileIds = this.selectedFiles[0]; |
|
186 |
+ } else if (type === 'all') { |
|
187 |
+ fileIds = this.dcry.files.map(file => file.fileId).join(','); |
|
188 |
+ } |
|
189 |
+ |
|
190 |
+ let isMultiple = fileIds.length > 1; |
|
191 |
+ const response = isMultiple ? await multiFileDownloadProc(fileIds) : await fileDownloadProc(fileIds); |
|
192 |
+ |
|
193 |
+ // 파일명 조회 |
|
194 |
+ let filename = isMultiple ? 'downloadFile.zip' : 'downloadFile.bin'; |
|
195 |
+ const filenameRegex = /file[Nn]ame[^;=\n]*=((['"]).*?\2|[^;\n]*)/; |
|
196 |
+ const matches = filenameRegex.exec(response.headers['content-disposition']); |
|
197 |
+ if (matches != null && matches[1]) { |
|
198 |
+ filename = matches[1].replace(/['"]/g, ''); |
|
199 |
+ } |
|
200 |
+ |
|
201 |
+ // 파일 다운로드 생성 |
|
202 |
+ url = window.URL.createObjectURL(new Blob([response.data])); |
|
203 |
+ link = document.createElement('a'); |
|
204 |
+ link.href = url; |
|
205 |
+ link.setAttribute('download', filename); |
|
206 |
+ document.body.appendChild(link); |
|
207 |
+ link.click(); |
|
208 |
+ } catch (error) { |
|
209 |
+ alert('파일 다운로드 중 오류가 발생했습니다.'); |
|
210 |
+ } finally { |
|
211 |
+ // 리소스 정리 |
|
212 |
+ setTimeout(() => { |
|
213 |
+ if (url) { |
|
214 |
+ window.URL.revokeObjectURL(url); |
|
215 |
+ } |
|
216 |
+ if (link && link.parentNode) { |
|
217 |
+ document.body.removeChild(link); |
|
218 |
+ } |
|
219 |
+ }, 100); |
|
220 |
+ } |
|
165 | 221 |
}, |
166 |
- watch: {}, |
|
167 |
- computed: { |
|
222 |
+ |
|
223 |
+ // 삭제 |
|
224 |
+ async fnDelete() { |
|
225 |
+ let isCheck = confirm("해당 페이지를 삭제하시겠습니까?"); |
|
226 |
+ if (!isCheck) { |
|
227 |
+ return; |
|
228 |
+ } |
|
229 |
+ |
|
230 |
+ try { |
|
231 |
+ const response = await deleteDcryProc(this.pageId); |
|
232 |
+ alert('해당 페이지를 삭제했습니다.'); |
|
233 |
+ |
|
234 |
+ this.fnMoveTo('PicHistorySearch'); |
|
235 |
+ } catch (error) { |
|
236 |
+ if (error.response) { |
|
237 |
+ alert(error.response.data.message); |
|
238 |
+ } |
|
239 |
+ console.error(error.message); |
|
240 |
+ } |
|
168 | 241 |
}, |
169 |
- mounted() { }, |
|
242 |
+ |
|
243 |
+ // 페이지 이동 |
|
244 |
+ fnMoveTo(page, id) { |
|
245 |
+ if (this.$isEmpty(id)) { |
|
246 |
+ this.$router.push({ name: page }); |
|
247 |
+ } else { |
|
248 |
+ this.$router.push({ name: page, query: { id: id } }); |
|
249 |
+ } |
|
250 |
+ }, |
|
251 |
+ }, |
|
170 | 252 |
}; |
171 | 253 |
</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/user/PicHistoryInsert.vue
+++ client/views/pages/user/PicHistoryInsert.vue
... | ... | @@ -15,34 +15,33 @@ |
15 | 15 |
<form class="insert-form mb-50"> |
16 | 16 |
<dl> |
17 | 17 |
<dd> |
18 |
- <label for="id" class="require">제목</label> |
|
19 |
- <div class="wfull"><input type="text" id="id" placeholder="제목을 입력하세요."></div> |
|
18 |
+ <label for="sj" class="require">제목</label> |
|
19 |
+ <div class="wfull"><input type="text" id="sj" placeholder="제목을 입력하세요." v-model="reqDTO.sj"></div> |
|
20 | 20 |
</dd> |
21 | 21 |
<div class="hr"></div> |
22 | 22 |
<dd> |
23 |
- <label for="year">생산연도</label> |
|
24 |
- <input type="text" id="year" placeholder="생산연도를 입력하세요"> |
|
23 |
+ <label for="prdctnYear">생산연도</label> |
|
24 |
+ <input type="text" id="prdctnYear" placeholder="생산연도를 입력하세요" v-model="reqDTO.prdctnYear"> |
|
25 | 25 |
</dd> |
26 | 26 |
<div class="hr"></div> |
27 | 27 |
<dd> |
28 |
- <label for="address">주소</label> |
|
29 |
- <div class="wfull"><input type="text" id="address" placeholder="주소를 입력하세요"></div> |
|
28 |
+ <label for="adres">주소</label> |
|
29 |
+ <div class="wfull"><input type="text" id="adres" placeholder="주소를 입력하세요" v-model="reqDTO.adres"></div> |
|
30 | 30 |
</dd> |
31 | 31 |
<div class="hr"></div> |
32 | 32 |
<dd> |
33 | 33 |
<label for="text">내용</label> |
34 | 34 |
<div class="wfull"> |
35 |
- <EditorComponent :contents="insertDTO.cn" /> |
|
35 |
+ <EditorComponent v-model:contents="reqDTO.cn" /> |
|
36 | 36 |
</div> |
37 | 37 |
</dd> |
38 | 38 |
<div class="hr"></div> |
39 | 39 |
<dd> |
40 | 40 |
<label for="category" class="flex align-center"> |
41 |
- <p>카테고리</p><button type="button" class="category-add" @click="openModal">추가하기</button> |
|
41 |
+ <p>카테고리</p><button type="button" class="category-add" @click="fnToggleModal">추가하기</button> |
|
42 | 42 |
</label> |
43 | 43 |
<ul class="category"> |
44 |
- <li v-for="(category, index) in selectedCtgries" :key="index"> {{ category }} <button type="button" class="cancel" @click="removeCategory(index)"><b>✕</b></button> |
|
45 |
- </li> |
|
44 |
+ <li v-for="(item, idx) of selectedCtgries" :key="idx">{{ item.ctgryNm }} <button type="button" class="cancel" @click="fnDelCtgry(item.ctgryId)"><b>✕</b></button></li> |
|
46 | 45 |
</ul> |
47 | 46 |
</dd> |
48 | 47 |
<div class="hr"></div> |
... | ... | @@ -54,22 +53,32 @@ |
54 | 53 |
<div class="invalid-feedback"><img :src="erroricon" alt=""><span>첨부파일은 건당 최대 10GB를 초과할 수 없습니다.</span></div> |
55 | 54 |
</li> |
56 | 55 |
<li class="file-insert"> |
57 |
- <input type="file" id="fileInput" class="file-input" multiple @change="showFileNames" accept="image/jpeg,image/png,image/gif,image/jpg"> |
|
56 |
+ <input type="file" id="fileInput" class="file-input" multiple accept="image/jpeg,image/png,image/gif,image/jpg" @change="handleFileSelect"> |
|
58 | 57 |
<label for="fileInput" class="file-label mb-20" @dragover.prevent="handleDragOver" @dragleave.prevent="handleDragLeave" @drop.prevent="handleDrop" :class="{ 'drag-over': isDragging }"> |
59 |
- <div class="flex-center align-center"><img :src="fileicon" alt=""> |
|
58 |
+ <div class="flex-center align-center"> |
|
59 |
+ <img :src="fileicon" alt=""> |
|
60 | 60 |
<p>파일첨부하기</p> |
61 | 61 |
</div> |
62 | 62 |
<p>파일을 첨부하시려면 이 영역으로 파일을 끌고 오거나 클릭해주세요</p> |
63 | 63 |
</label> |
64 | 64 |
<p class="mb-10">파일목록</p> |
65 | 65 |
<div id="fileNames" class="file-names"> |
66 |
- <span v-if="fileNames.length === 0">선택된 파일이 없습니다.</span> |
|
67 |
- <div v-for="(file, index) in fileNames" :key="index" class="flex-sp-bw mb-5 file-wrap"> |
|
66 |
+ <div v-if="reqDTO.files.length === 0 && multipartFiles.length === 0">선택된 파일이 없습니다.</div> |
|
67 |
+ <!-- 새로 추가된 파일 목록 --> |
|
68 |
+ <div v-for="(file, idx) of multipartFiles" :key="idx" class="flex-sp-bw mb-5 file-wrap"> |
|
68 | 69 |
<div class="file-name"> |
69 |
- <img :src="file.icon" alt="fileicon"> |
|
70 |
+ <img src="/client/resources/images/icon/imgicon.png" alt="fileicon"> |
|
70 | 71 |
<p>{{ file.name }}</p> |
71 | 72 |
</div> |
72 |
- <button type="button" class="cancel" @click="removeFile(index)"><b>✕</b></button> |
|
73 |
+ <button type="button" class="cancel" @click="fnDelFile('new', idx)"><b>✕</b></button> |
|
74 |
+ </div> |
|
75 |
+ <!-- 기존 등록된 파일 목록 --> |
|
76 |
+ <div v-for="(file, idx) of reqDTO.files" :key="idx" class="flex-sp-bw mb-5 file-wrap"> |
|
77 |
+ <div class="file-name"> |
|
78 |
+ <img src="/client/resources/images/icon/imgicon.png" alt="fileicon"> |
|
79 |
+ <p>{{ file.fileNm }}</p> |
|
80 |
+ </div> |
|
81 |
+ <button type="button" class="cancel" @click="fnDelFile('old', file.fileId)"><b>✕</b></button> |
|
73 | 82 |
</div> |
74 | 83 |
</div> |
75 | 84 |
</li> |
... | ... | @@ -79,18 +88,21 @@ |
79 | 88 |
</form> |
80 | 89 |
<div class="btn-group flex-center"> |
81 | 90 |
<button type="button" class="cancel" @click="fnMoveTo('PicHistorySearch')">취소</button> |
82 |
- <button type="button" class="register" @click="submitForm">등록</button> |
|
91 |
+ <button type="button" class="register" @click="submitForm"> |
|
92 |
+ <span v-if="$isEmpty(pageId)">등록</span> |
|
93 |
+ <span v-else>수정</span> |
|
94 |
+ </button> |
|
83 | 95 |
</div> |
84 | 96 |
</div> |
85 |
- <CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" /> |
|
97 |
+ <CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" @addCtgries="fnAddCtgries" /> |
|
86 | 98 |
</template> |
87 | 99 |
<script> |
88 |
-import axios from 'axios'; |
|
89 |
-import apiClient from '../../../resources/api'; |
|
90 | 100 |
import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'; |
91 | 101 |
// COMPONENT |
92 | 102 |
import EditorComponent from '../../component/EditorComponent.vue'; |
93 | 103 |
import CategorySelectModal from '../../component/modal/CategorySelectModal.vue'; |
104 |
+// API |
|
105 |
+import { findDcryProc, saveDcry, updateDcry } from '@/resources/api/dcry'; |
|
94 | 106 |
|
95 | 107 |
export default { |
96 | 108 |
components: { |
... | ... | @@ -111,37 +123,90 @@ |
111 | 123 |
fileicon: 'client/resources/images/icon/file.png', |
112 | 124 |
searchicon: 'client/resources/images/icon/search.png', |
113 | 125 |
|
126 |
+ pageId: null, |
|
127 |
+ |
|
114 | 128 |
isModalOpen: false, |
115 | 129 |
isDragging: false, |
116 | 130 |
|
117 |
- items: [ |
|
118 |
- { id: 1, category: '카테고리 1', selected: false }, |
|
119 |
- { id: 2, category: '카테고리 2', selected: false }, |
|
120 |
- { id: 3, category: '카테고리 3', selected: false }, |
|
121 |
- ], |
|
122 | 131 |
fileNames: [], |
123 |
- insertDTO: { |
|
124 |
- sj: null, //제목 |
|
125 |
- cn: null, //내용 |
|
126 |
- adres: null, // 주소 |
|
127 |
- prdctnYear: null, // 생산연도 |
|
128 |
- ty: 'P', // 타입 ( P: 사진, V: 영상 ) |
|
129 |
- ctgryIds: null, // 카테고리 정보 |
|
132 |
+ |
|
133 |
+ // 등록/수정 요청 객체 |
|
134 |
+ reqDTO: { |
|
135 |
+ dcryId: null, |
|
136 |
+ sj: null, |
|
137 |
+ cn: null, |
|
138 |
+ adres: null, |
|
139 |
+ prdctnYear: null, |
|
140 |
+ ty: 'P', |
|
141 |
+ fileId: null, |
|
142 |
+ files: [], |
|
143 |
+ ctgryIds: [], |
|
130 | 144 |
}, |
131 | 145 |
|
132 |
- selectedFiles: [], |
|
146 |
+ multipartFiles: [], |
|
133 | 147 |
selectedCtgries: [], // 카테고리 목록 |
134 | 148 |
}; |
135 | 149 |
}, |
136 |
- computed: { |
|
137 |
- filteredItems() { |
|
138 |
- // This could be modified to support filtering based on searchQuery |
|
139 |
- return this.items.filter(item => |
|
140 |
- item.category.includes(this.searchQuery) |
|
141 |
- ); |
|
150 |
+ |
|
151 |
+ computed: {}, |
|
152 |
+ |
|
153 |
+ created() { |
|
154 |
+ this.pageId = this.$route.query.id; |
|
155 |
+ if (!this.$isEmpty(this.pageId)) { |
|
156 |
+ this.fnFindDcry(); // 상세 조회 |
|
142 | 157 |
} |
143 | 158 |
}, |
159 |
+ |
|
144 | 160 |
methods: { |
161 |
+ // 상세 조회 |
|
162 |
+ async fnFindDcry() { |
|
163 |
+ try { |
|
164 |
+ const response = await findDcryProc(this.pageId); |
|
165 |
+ this.copyToDcryReqDTO(response.data.data.dcry); |
|
166 |
+ } catch (error) { |
|
167 |
+ alert('조회중 오류가 발생했습니다.'); |
|
168 |
+ this.fnMoveTo('PicHistorySearch'); // 목록으로 이동 |
|
169 |
+ |
|
170 |
+ if (error.response) { |
|
171 |
+ alert(error.response.data.message); |
|
172 |
+ } |
|
173 |
+ console.error(error.message); |
|
174 |
+ } |
|
175 |
+ }, |
|
176 |
+ |
|
177 |
+ // dcry > reqDTO |
|
178 |
+ copyToDcryReqDTO(dcry) { |
|
179 |
+ const copyFields = Object.keys(this.reqDTO).filter(key => key !== 'dcryId' && key !== 'ty' && key !== 'files'); |
|
180 |
+ copyFields.forEach(field => { |
|
181 |
+ this.reqDTO[field] = this.$isEmpty(dcry[field]) ? null : dcry[field]; |
|
182 |
+ }); |
|
183 |
+ |
|
184 |
+ this.reqDTO.ty = 'P'; // 사진기록물 |
|
185 |
+ this.reqDTO.files = dcry.files.length > 0 ? dcry.files : []; // 기존 첨부파일 |
|
186 |
+ |
|
187 |
+ this.multipartFiles = []; |
|
188 |
+ this.selectedCtgries = dcry.ctgrys.length > 0 ? dcry.ctgrys : []; |
|
189 |
+ |
|
190 |
+ console.log(this.reqDTO); |
|
191 |
+ }, |
|
192 |
+ |
|
193 |
+ // 카테고리 모달 열기/닫기 |
|
194 |
+ fnToggleModal() { |
|
195 |
+ this.isModalOpen = !this.isModalOpen; |
|
196 |
+ }, |
|
197 |
+ |
|
198 |
+ // 카테고리 등록 |
|
199 |
+ fnAddCtgries(selectedCtgries) { |
|
200 |
+ this.selectedCtgries = [...this.selectedCtgries, ...selectedCtgries]; |
|
201 |
+ |
|
202 |
+ this.fnToggleModal(); // 카테고리 모달 닫기 |
|
203 |
+ }, |
|
204 |
+ |
|
205 |
+ // 카테고리 삭제 |
|
206 |
+ fnDelCtgry(id) { |
|
207 |
+ this.selectedCtgries = this.selectedCtgries.filter(item => item.ctgryId !== id); |
|
208 |
+ }, |
|
209 |
+ |
|
145 | 210 |
// 드래그 앤 드롭 이벤트 핸들러 |
146 | 211 |
handleDragOver(event) { |
147 | 212 |
this.isDragging = true; |
... | ... | @@ -151,7 +216,14 @@ |
151 | 216 |
}, |
152 | 217 |
handleDrop(event) { |
153 | 218 |
this.isDragging = false; |
219 |
+ |
|
154 | 220 |
const files = event.dataTransfer.files; |
221 |
+ if (files.length > 0) { |
|
222 |
+ this.processFiles(files); |
|
223 |
+ } |
|
224 |
+ }, |
|
225 |
+ handleFileSelect(event) { |
|
226 |
+ const files = event.target.files; |
|
155 | 227 |
if (files.length > 0) { |
156 | 228 |
this.processFiles(files); |
157 | 229 |
} |
... | ... | @@ -159,169 +231,112 @@ |
159 | 231 |
|
160 | 232 |
// 파일 업로드 처리 함수 |
161 | 233 |
processFiles(files) { |
162 |
- // 파일 타입 검증 (이미지 파일만 허용) |
|
163 |
- const allowedTypes = ['jpg', 'jpeg', 'png', 'gif']; |
|
234 |
+ const allowedTypes = ['jpg', 'jpeg', 'png', 'gif']; // 이미지 파일만 허용 |
|
164 | 235 |
const maxSize = 10 * 1024 * 1024 * 1024; // 10GB |
165 | 236 |
|
166 |
- for (let i = 0; i < files.length; i++) { |
|
167 |
- const fileType = files[i].name.split('.').pop().toLowerCase(); |
|
237 |
+ for (let file of files) { |
|
238 |
+ const fileType = file.name.split('.').pop().toLowerCase(); |
|
239 |
+ |
|
240 |
+ // 파일 타입 검증 |
|
168 | 241 |
if (!allowedTypes.includes(fileType)) { |
169 |
- alert(`${files[i].name} 파일은 허용되지 않는 형식입니다. 이미지 파일(jpg, jpeg, png, gif)만 업로드 가능합니다.`); |
|
242 |
+ alert(`${file.name} 파일은 허용되지 않는 형식입니다. 이미지 파일(jpg, jpeg, png, gif)만 업로드 가능합니다.`); |
|
170 | 243 |
return; |
171 | 244 |
} |
172 | 245 |
|
173 |
- // 파일 크기 제한 체크 (10GB) |
|
174 |
- if (files[i].size > maxSize) { |
|
175 |
- alert(`${files[i].name} 파일이 10GB를 초과합니다.`); |
|
246 |
+ // 파일 크기 제한 검증 |
|
247 |
+ if (file.size > maxSize) { |
|
248 |
+ alert(`${file.name} 파일이 10GB를 초과합니다.`); |
|
176 | 249 |
return; |
177 | 250 |
} |
178 |
- } |
|
179 | 251 |
|
180 |
- // 실제 File 객체들을 저장 |
|
181 |
- for (let i = 0; i < files.length; i++) { |
|
182 |
- this.selectedFiles.push(files[i]); |
|
183 |
- } |
|
184 |
- |
|
185 |
- // UI에 표시할 파일 정보 저장 |
|
186 |
- for (let i = 0; i < files.length; i++) { |
|
187 |
- const file = files[i]; |
|
188 |
- const fileType = file.name.split('.').pop().toLowerCase(); // 파일 확장자 추출 |
|
189 |
- |
|
190 |
- // 파일 타입에 따른 아이콘 선택 |
|
191 |
- let iconPath = this.fileicon; // 기본 아이콘 |
|
192 |
- |
|
193 |
- if (['jpg', 'jpeg', 'png', 'gif'].includes(fileType)) { |
|
194 |
- iconPath = 'client/resources/images/icon/imgicon.png'; |
|
195 |
- } else if (['pdf'].includes(fileType)) { |
|
196 |
- iconPath = 'client/resources/images/icon/pdficon.png'; |
|
197 |
- } else if (['xls', 'xlsx'].includes(fileType)) { |
|
198 |
- iconPath = 'client/resources/images/icon/excelicon.png'; |
|
199 |
- } else if (['hwp'].includes(fileType)) { |
|
200 |
- iconPath = 'client/resources/images/icon/hwpicon.png'; |
|
201 |
- } |
|
202 |
- |
|
203 |
- // 파일 이름과 아이콘을 목록에 추가 |
|
204 |
- this.fileNames.push({ |
|
205 |
- name: file.name, |
|
206 |
- icon: iconPath, |
|
207 |
- size: this.formatFileSize(file.size) |
|
208 |
- }); |
|
252 |
+ this.multipartFiles.push(file); |
|
209 | 253 |
} |
210 | 254 |
}, |
211 | 255 |
|
212 |
- fnToggleModal(selectedCtgryIds) { |
|
213 |
- this.isModalOpen = !this.isModalOpen; |
|
214 |
- if (selectedCtgryIds && selectedCtgryIds.length > 0) { |
|
215 |
- this.insertDTO.ctgryIds = selectedCtgryIds; |
|
256 |
+ // 파일 삭제 |
|
257 |
+ fnDelFile(type, separator) { |
|
258 |
+ if (type === 'new') { |
|
259 |
+ this.multipartFiles.splice(separator, 1); |
|
260 |
+ } else if (type === 'old') { |
|
261 |
+ this.reqDTO.files = this.reqDTO.files.filter(item => item.fileId !== separator); |
|
216 | 262 |
} |
217 |
- }, |
|
218 |
- removeCategory(index) { |
|
219 |
- // Remove category from the list |
|
220 |
- this.selectedCtgries.splice(index, 1); |
|
221 |
- }, |
|
222 |
- openModal() { |
|
223 |
- this.isModalOpen = true; |
|
224 |
- }, |
|
225 |
- closeModal() { |
|
226 |
- this.isModalOpen = false; |
|
227 |
- }, |
|
228 |
- showFileNames(event) { |
|
229 |
- const files = event.target.files; |
|
230 |
- if (files.length > 0) { |
|
231 |
- this.processFiles(files); |
|
232 |
- } |
|
233 |
- }, |
|
234 |
- removeFile(index) { |
|
235 |
- // UI 목록과 실제 파일 객체 목록에서 모두 제거 |
|
236 |
- this.fileNames.splice(index, 1); |
|
237 |
- this.selectedFiles.splice(index, 1); |
|
238 |
- }, |
|
239 |
- formatFileSize(bytes) { |
|
240 |
- if (bytes === 0) return '0 Bytes'; |
|
241 |
- const k = 1024; |
|
242 |
- const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
|
243 |
- const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
244 |
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
245 | 263 |
}, |
246 | 264 |
|
247 | 265 |
// 등록 |
248 |
- submitForm() { |
|
249 |
- const vm = this; |
|
250 |
- |
|
251 |
- // 폼 요소들의 값을 가져오기 |
|
252 |
- const titleInput = document.getElementById('id'); |
|
253 |
- const yearInput = document.getElementById('year'); |
|
254 |
- const addressInput = document.getElementById('address'); |
|
255 |
- |
|
256 |
- // insertDTO 업데이트 |
|
257 |
- vm.insertDTO.sj = titleInput ? titleInput.value : null; |
|
258 |
- vm.insertDTO.prdctnYear = yearInput ? yearInput.value : null; |
|
259 |
- vm.insertDTO.adres = addressInput ? addressInput.value : null; |
|
260 |
- |
|
266 |
+ async submitForm() { |
|
261 | 267 |
// 유효성 검사 |
262 |
- if (!vm.insertDTO.sj) { |
|
268 |
+ if (!this.reqDTO.sj) { |
|
263 | 269 |
alert("제목을 입력해 주세요."); |
264 | 270 |
return; |
265 | 271 |
} |
266 |
- if (vm.selectedFiles.length < 1) { |
|
272 |
+ if (this.$isEmpty(this.pageId) && this.multipartFiles.length < 1) { |
|
267 | 273 |
alert("파일을 1개 이상 첨부해 주세요."); |
268 | 274 |
return; |
269 | 275 |
} |
270 | 276 |
|
271 |
- // 데이터 세팅 |
|
272 |
- const formData = new FormData(); |
|
277 |
+ try { |
|
278 |
+ const formData = new FormData(); |
|
273 | 279 |
|
274 |
- // 텍스트 데이터 추가 |
|
275 |
- formData.append('sj', vm.insertDTO.sj); |
|
276 |
- formData.append('cn', vm.insertDTO.cn || ''); |
|
277 |
- formData.append('adres', vm.insertDTO.adres || ''); |
|
278 |
- formData.append('prdctnYear', vm.insertDTO.prdctnYear || ''); |
|
279 |
- formData.append('ty', vm.insertDTO.ty); |
|
280 |
+ // 텍스트 데이터 추가 |
|
281 |
+ formData.append('sj', this.reqDTO.sj); |
|
282 |
+ formData.append('cn', this.reqDTO.cn); |
|
283 |
+ formData.append('adres', this.reqDTO.adres); |
|
284 |
+ formData.append('prdctnYear', this.reqDTO.prdctnYear); |
|
285 |
+ formData.append('ty', this.reqDTO.ty); |
|
280 | 286 |
|
281 |
- // 카테고리 IDs 추가 |
|
282 |
- if (vm.insertDTO.ctgryIds && vm.insertDTO.ctgryIds.length > 0) { |
|
283 |
- // 백엔드 요구사항에 따라 조정 필요 |
|
284 |
- vm.insertDTO.ctgryIds.forEach((id, index) => { |
|
285 |
- formData.append(`ctgryIds[${index}]`, id); |
|
286 |
- }); |
|
287 |
- } |
|
287 |
+ // 게시물 아이디 |
|
288 |
+ if (!this.$isEmpty(this.pageId)) { |
|
289 |
+ formData.append('dcryId', this.pageId); |
|
290 |
+ } |
|
288 | 291 |
|
289 |
- // 파일 추가 |
|
290 |
- for (let i = 0; i < vm.selectedFiles.length; i++) { |
|
291 |
- formData.append("multipartFiles", vm.selectedFiles[i]); |
|
292 |
- } |
|
292 |
+ // 파일 아이디 |
|
293 |
+ if (!this.$isEmpty(this.reqDTO.fileId)) { |
|
294 |
+ formData.append('fileId', this.reqDTO.fileId); |
|
295 |
+ } |
|
293 | 296 |
|
294 |
- // API 통신 |
|
295 |
- axios({ |
|
296 |
- url: "/dcry/saveDcry.file", |
|
297 |
- method: "post", |
|
298 |
- headers: { |
|
299 |
- "Content-Type": "multipart/form-data", |
|
300 |
- }, |
|
301 |
- data: formData, |
|
302 |
- }).then(response => { |
|
297 |
+ // 카테고리 Ids 추가 |
|
298 |
+ if (this.selectedCtgries && this.selectedCtgries.length > 0) { |
|
299 |
+ for (let ctgry of this.selectedCtgries) { |
|
300 |
+ formData.append("ctgryIds", ctgry.ctgryId); |
|
301 |
+ } |
|
302 |
+ } |
|
303 |
+ |
|
304 |
+ // 파일 추가 |
|
305 |
+ for (let file of this.multipartFiles) { |
|
306 |
+ formData.append("multipartFiles", file); |
|
307 |
+ } |
|
308 |
+ |
|
309 |
+ // 기존파일 수정 |
|
310 |
+ if (!this.$isEmpty(this.pageId) && this.reqDTO.files.length > 0) { |
|
311 |
+ for (let file of this.reqDTO.files) { |
|
312 |
+ formData.append("files", file.fileId); |
|
313 |
+ } |
|
314 |
+ } |
|
315 |
+ |
|
316 |
+ // API 통신 |
|
317 |
+ const response = this.$isEmpty(this.pageId) ? await saveDcry(formData) : await updateDcry(formData); |
|
303 | 318 |
let result = response.data; |
304 | 319 |
let id = result.data.dcryId; |
305 |
- alert("등록 되었습니다."); |
|
320 |
+ alert(this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다."); |
|
306 | 321 |
|
307 | 322 |
// 상세 페이지로 이동 |
308 |
- vm.fnMoveTo('PicHistoryDetail', id); |
|
309 |
- }).catch(error => { |
|
323 |
+ this.fnMoveTo('PicHistoryDetail', id); |
|
324 |
+ } catch (error) { |
|
310 | 325 |
if (error.response) { |
311 | 326 |
alert(error.response.data.message); |
312 | 327 |
} else { |
313 | 328 |
alert("에러가 발생했습니다."); |
314 | 329 |
} |
315 | 330 |
console.error(error.message); |
316 |
- }); |
|
331 |
+ }; |
|
317 | 332 |
}, |
318 | 333 |
|
319 | 334 |
// 페이지 이동 |
320 | 335 |
fnMoveTo(page, id) { |
321 |
- if (id !== null || id !== '') { |
|
322 |
- this.$router.push({ name: page, query: { id: id } }); |
|
323 |
- } else { |
|
336 |
+ if (this.$isEmpty(id)) { |
|
324 | 337 |
this.$router.push({ name: page }); |
338 |
+ } else { |
|
339 |
+ this.$router.push({ name: page, query: { id: id } }); |
|
325 | 340 |
} |
326 | 341 |
} |
327 | 342 |
} |
--- webpack.config.js
+++ webpack.config.js
... | ... | @@ -1,6 +1,7 @@ |
1 | 1 |
const HtmlWebpackPlugin = require('html-webpack-plugin'); |
2 | 2 |
const { VueLoaderPlugin } = require('vue-loader'); |
3 | 3 |
const webpack = require('webpack'); |
4 |
+const path = require('path'); |
|
4 | 5 |
|
5 | 6 |
const { PROJECT_NAME, BASE_DIR, SERVICE_STATUS } = require('./Global'); |
6 | 7 |
|
... | ... | @@ -13,6 +14,13 @@ |
13 | 14 |
app: [`${BASE_DIR}/client/views/index.js`] |
14 | 15 |
}, |
15 | 16 |
|
17 |
+ resolve: { |
|
18 |
+ extensions: ['.js', '.vue', '.json'], |
|
19 |
+ alias: { |
|
20 |
+ '@': path.resolve(__dirname, `${BASE_DIR}/client`), // @ 경로를 client 폴더로 지정 |
|
21 |
+ } |
|
22 |
+ }, |
|
23 |
+ |
|
16 | 24 |
module: { |
17 | 25 |
rules: [{ |
18 | 26 |
test: /\.vue?$/, |
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?