
--- client/resources/js/cmmnPlugin.js
+++ client/resources/js/cmmnPlugin.js
... | ... | @@ -25,7 +25,53 @@ |
25 | 25 |
|
26 | 26 |
// 정규식을 사용하여 모든 HTML 태그 제거 |
27 | 27 |
Vue.config.globalProperties.$stripHtml = (html) => { |
28 |
- return html.replace(/<[^>]*>/g, ''); |
|
29 |
- } |
|
28 |
+ if (!html) return ''; |
|
29 |
+ |
|
30 |
+ // 환경 확인 (브라우저 vs 서버) |
|
31 |
+ if (typeof window !== 'undefined' && window.DOMParser) { |
|
32 |
+ try { |
|
33 |
+ // 브라우저 환경에서는 DOMParser 사용 |
|
34 |
+ const doc = new DOMParser().parseFromString(html, 'text/html'); |
|
35 |
+ let text = doc.body.textContent || ''; |
|
36 |
+ return text.replace(/\s+/g, ' ').trim(); |
|
37 |
+ } catch (e) { |
|
38 |
+ // DOMParser 실패시 정규식 방식으로 폴백 |
|
39 |
+ console.warn('DOMParser failed, falling back to regex', e); |
|
40 |
+ } |
|
41 |
+ } |
|
42 |
+ |
|
43 |
+ // 서버 환경이거나 DOMParser 실패 시 정규식 사용 |
|
44 |
+ let text = html.replace(/<[^>]*>/g, ''); |
|
45 |
+ |
|
46 |
+ // 일반적인 HTML 엔티티 처리 |
|
47 |
+ const entityMap = { |
|
48 |
+ ' ': ' ', |
|
49 |
+ '&': '&', |
|
50 |
+ '<': '<', |
|
51 |
+ '>': '>', |
|
52 |
+ '"': '"', |
|
53 |
+ ''': "'", |
|
54 |
+ '—': '—', |
|
55 |
+ '–': '–', |
|
56 |
+ '°': '°' |
|
57 |
+ }; |
|
58 |
+ |
|
59 |
+ // 정의된 엔티티 변환 |
|
60 |
+ Object.keys(entityMap).forEach(entity => { |
|
61 |
+ const regex = new RegExp(entity, 'g'); |
|
62 |
+ text = text.replace(regex, entityMap[entity]); |
|
63 |
+ }); |
|
64 |
+ |
|
65 |
+ // 남은 숫자 엔티티 제거 ({ 형식) |
|
66 |
+ text = text.replace(/&#[0-9]+;/g, ''); |
|
67 |
+ |
|
68 |
+ // 남은 이름 있는 엔티티 제거 (… 등) |
|
69 |
+ text = text.replace(/&[a-zA-Z]+;/g, ''); |
|
70 |
+ |
|
71 |
+ // 연속된 공백 문자 하나로 처리 |
|
72 |
+ text = text.replace(/\s+/g, ' ').trim(); |
|
73 |
+ |
|
74 |
+ return text; |
|
75 |
+ }; |
|
30 | 76 |
} |
31 | 77 |
}(파일 끝에 줄바꿈 문자 없음) |
--- client/views/component/listLayout/CardStyleComponent.vue
+++ client/views/component/listLayout/CardStyleComponent.vue
... | ... | @@ -4,7 +4,7 @@ |
4 | 4 |
<div class="result-box"> |
5 | 5 |
<div class="main-img"> |
6 | 6 |
<img v-if="name === 'M'" :src="getYouTubeThumbnail(item.link)" alt="영상 썸네일"> |
7 |
- <img v-else-if="!$isEmpty(item.thumbnail)" :src="item.thumbnail.filePath" :alt="item.sj + ' 썸네일'"> |
|
7 |
+ <img v-else-if="!$isEmpty(item.thumbnail)" :src="item.thumbnail.filePath" :alt="item.sj + ' 썸네일'" loading="lazy"> |
|
8 | 8 |
<img v-else src="client/resources/images/img6.png" alt="Not found image"> |
9 | 9 |
</div> |
10 | 10 |
<div class="text-box"> |
--- client/views/component/modal/CategorySelectModal.vue
+++ client/views/component/modal/CategorySelectModal.vue
... | ... | @@ -6,8 +6,8 @@ |
6 | 6 |
<button class="closebtn" @click="$emit('toggleModal')">✕</button> |
7 | 7 |
</div> |
8 | 8 |
<div class="modal-search flex-center mb-20"> |
9 |
- <input type="text" placeholder="카테고리명을 입력하세요." v-model="searchReqDTO.searchText" @keyup.enter="fnFindAllCategory"> |
|
10 |
- <button type="button" class="search-btn" @click="fnFindAllCategory"> |
|
9 |
+ <input type="text" placeholder="카테고리명을 입력하세요." v-model="searchReqDTO.searchText" @keyup.enter="fnChnageReqDTO"> |
|
10 |
+ <button type="button" class="search-btn" @click="fnChnageReqDTO"> |
|
11 | 11 |
<img :src="searchicon" alt=""> |
12 | 12 |
<p>검색</p> |
13 | 13 |
</button> |
... | ... | @@ -78,12 +78,20 @@ |
78 | 78 |
}, |
79 | 79 |
|
80 | 80 |
created() { |
81 |
- this.fnFindAllCategory(); // 목록 조회 |
|
81 |
+ this.fnSearch(); // 목록 조회 |
|
82 | 82 |
}, |
83 | 83 |
|
84 | 84 |
methods: { |
85 |
+ // 검색 조건이 변경된 경우 |
|
86 |
+ fnChnageReqDTO() { |
|
87 |
+ this.searchReqDTO.currentPage = 1; |
|
88 |
+ this.$nextTick(() => { |
|
89 |
+ this.fnSearch(); |
|
90 |
+ }); |
|
91 |
+ }, |
|
92 |
+ |
|
85 | 93 |
// 목록 조회 |
86 |
- async fnFindAllCategory() { |
|
94 |
+ async fnSearch() { |
|
87 | 95 |
try { |
88 | 96 |
if (this.searchReqDTO.hasOwnProperty('ctgryIds')) { |
89 | 97 |
delete this.searchReqDTO.ctgryIds; |
... | ... | @@ -126,7 +134,7 @@ |
126 | 134 |
this.searchReqDTO.currentPage = Number(currentPage); |
127 | 135 |
|
128 | 136 |
this.$nextTick(() => { |
129 |
- this.fnFindAllCategory(); |
|
137 |
+ this.fnSearch(); |
|
130 | 138 |
}); |
131 | 139 |
}, |
132 | 140 |
} |
--- client/views/pages/bbsMediaVido/MediaVideoInsert.vue
+++ client/views/pages/bbsMediaVido/MediaVideoInsert.vue
... | ... | @@ -26,7 +26,11 @@ |
26 | 26 |
<div class="hr"></div> |
27 | 27 |
<dd> |
28 | 28 |
<label for="link">주소</label> |
29 |
- <div class="wfull"><input type="text" id="link" placeholder="URL 주소를 입력하세요" v-model="requestDTO.link"></div> |
|
29 |
+ <input type="text" id="link" class="invalid-url" placeholder="유튜브 URL 주소를 입력하세요" v-model="requestDTO.link"> |
|
30 |
+ <div class="invalid-feedback border"> |
|
31 |
+ <img :src="erroricon" alt=""> |
|
32 |
+ <span>유튜브 URL만 입력 가능합니다.</span> |
|
33 |
+ </div> |
|
30 | 34 |
</dd> |
31 | 35 |
<div class="hr"></div> |
32 | 36 |
<dd> |
... | ... | @@ -97,6 +101,7 @@ |
97 | 101 |
ctgryIds: [], // 카테고리 정보 |
98 | 102 |
}, |
99 | 103 |
|
104 |
+ isValidYoutubeURL: true, |
|
100 | 105 |
selectedCtgries: [], // 카테고리 목록 |
101 | 106 |
}; |
102 | 107 |
}, |
... | ... | @@ -168,6 +173,13 @@ |
168 | 173 |
return; |
169 | 174 |
} |
170 | 175 |
} |
176 |
+ if (!this.$isEmpty(this.requestDTO.link)) { |
|
177 |
+ const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/shorts\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})(\S*)?$/; |
|
178 |
+ if (!youtubeRegex.test(this.requestDTO.link)) { |
|
179 |
+ alert("주소는 유튜브 URL만 입력 가능합니다."); |
|
180 |
+ return; |
|
181 |
+ } |
|
182 |
+ } |
|
171 | 183 |
|
172 | 184 |
try { |
173 | 185 |
if (this.$isEmpty(this.pageId)) { |
... | ... | @@ -212,7 +224,12 @@ |
212 | 224 |
// 생산연도 입력 제한 |
213 | 225 |
onlyNumberInput() { |
214 | 226 |
this.requestDTO.prdctnYear = this.requestDTO.prdctnYear.replace(/[^0-9]/g, ''); |
215 |
- } |
|
227 |
+ }, |
|
216 | 228 |
} |
217 | 229 |
}; |
218 |
-</script>(파일 끝에 줄바꿈 문자 없음) |
|
230 |
+</script> |
|
231 |
+<style scoped> |
|
232 |
+.invalid-url { |
|
233 |
+ width: 50%; |
|
234 |
+} |
|
235 |
+</style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/bbsNesDta/NewsReleaseInsert.vue
+++ client/views/pages/bbsNesDta/NewsReleaseInsert.vue
... | ... | @@ -286,6 +286,13 @@ |
286 | 286 |
return; |
287 | 287 |
} |
288 | 288 |
} |
289 |
+ if (!this.$isEmpty(this.requestDTO.link)) { |
|
290 |
+ const urlRegex = /^(https?:\/\/)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/; |
|
291 |
+ if (!urlRegex.test(this.requestDTO.link)) { |
|
292 |
+ alert("링크는 url만 입력 가능합니다."); |
|
293 |
+ return; |
|
294 |
+ } |
|
295 |
+ } |
|
289 | 296 |
|
290 | 297 |
try { |
291 | 298 |
const formData = new FormData(); |
... | ... | @@ -324,6 +331,11 @@ |
324 | 331 |
// 파일 추가 |
325 | 332 |
if (this.multipartFiles.length > 0) { |
326 | 333 |
formData.append("multipartFiles", this.multipartFiles[0]); |
334 |
+ |
|
335 |
+ // 썸네일 정보 추가 |
|
336 |
+ if (this.selectedThumb !== null) { |
|
337 |
+ formData.append('selectedThumb', this.multipartFiles[0].name); |
|
338 |
+ } |
|
327 | 339 |
} |
328 | 340 |
|
329 | 341 |
// 기존파일 수정 |
... | ... | @@ -331,11 +343,6 @@ |
331 | 343 |
for (let file of this.requestDTO.files) { |
332 | 344 |
formData.append("files", file.fileOrdr); |
333 | 345 |
} |
334 |
- } |
|
335 |
- |
|
336 |
- // 썸네일 정보 추가 |
|
337 |
- if (this.selectedThumb !== null) { |
|
338 |
- formData.append('selectedThumb', this.multipartFiles[0].name); |
|
339 | 346 |
} |
340 | 347 |
|
341 | 348 |
// API 통신 |
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?