
+++ client/views/component/VideoThumbnail.vue
... | ... | @@ -0,0 +1,127 @@ |
1 | +<template> | |
2 | + <div> | |
3 | + <img v-if="thumbnailUrl" :src="thumbnailUrl" alt="비디오 썸네일"> | |
4 | + <div v-else-if="error" class="error">썸네일 생성 실패</div> | |
5 | + <div v-else class="loading">로딩 중...</div> | |
6 | + </div> | |
7 | +</template> | |
8 | +<script> | |
9 | +export default { | |
10 | + data() { | |
11 | + return { | |
12 | + thumbnailUrl: null, | |
13 | + error: false | |
14 | + } | |
15 | + }, | |
16 | + props: { | |
17 | + filePath: String | |
18 | + }, | |
19 | + methods: { | |
20 | + createThumbnail(videoBlob) { | |
21 | + return new Promise((resolve, reject) => { | |
22 | + // 동영상 요소 생성 | |
23 | + const video = document.createElement('video'); | |
24 | + video.style.display = 'none'; | |
25 | + document.body.appendChild(video); | |
26 | + | |
27 | + // 캔버스 요소 생성 | |
28 | + const canvas = document.createElement('canvas'); | |
29 | + canvas.width = 320; | |
30 | + canvas.height = 180; | |
31 | + | |
32 | + // 동영상 파일로부터 URL 생성 | |
33 | + const videoUrl = URL.createObjectURL(videoBlob); | |
34 | + video.src = videoUrl; | |
35 | + | |
36 | + // 로드 시간 제한 설정 (10초) | |
37 | + const timeout = setTimeout(() => { | |
38 | + URL.revokeObjectURL(videoUrl); | |
39 | + document.body.removeChild(video); | |
40 | + reject(new Error('비디오 로드 시간 초과')); | |
41 | + }, 10000); | |
42 | + | |
43 | + // 오류 처리 | |
44 | + video.onerror = (e) => { | |
45 | + clearTimeout(timeout); | |
46 | + URL.revokeObjectURL(videoUrl); | |
47 | + document.body.removeChild(video); | |
48 | + reject(new Error('비디오 로드 실패: ' + e.message)); | |
49 | + }; | |
50 | + | |
51 | + // 메타데이터 로드 후 처리 | |
52 | + video.onloadedmetadata = () => { | |
53 | + // 첫 프레임이나 특정 시점으로 이동 | |
54 | + video.currentTime = 0.5; | |
55 | + }; | |
56 | + | |
57 | + // 특정 시간으로 이동 완료 후 썸네일 생성 | |
58 | + video.onseeked = () => { | |
59 | + try { | |
60 | + clearTimeout(timeout); | |
61 | + | |
62 | + const ctx = canvas.getContext('2d'); | |
63 | + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | |
64 | + | |
65 | + // 썸네일 URL 생성 | |
66 | + const thumbnailUrl = canvas.toDataURL('image/jpeg', 0.7); | |
67 | + | |
68 | + // 리소스 정리 | |
69 | + URL.revokeObjectURL(videoUrl); | |
70 | + document.body.removeChild(video); | |
71 | + | |
72 | + resolve(thumbnailUrl); | |
73 | + } catch (error) { | |
74 | + URL.revokeObjectURL(videoUrl); | |
75 | + document.body.removeChild(video); | |
76 | + reject(error); | |
77 | + } | |
78 | + }; | |
79 | + }); | |
80 | + } | |
81 | + }, | |
82 | + async mounted() { | |
83 | + try { | |
84 | + console.log('비디오 파일 경로:', this.filePath); | |
85 | + | |
86 | + // 백엔드에서 파일 가져오기 | |
87 | + const response = await fetch(this.filePath, { | |
88 | + // credentials: 'include', // 필요한 경우 쿠키 포함 | |
89 | + // mode: 'cors', // CORS 모드 설정 | |
90 | + }); | |
91 | + | |
92 | + if (!response.ok) { | |
93 | + throw new Error(`HTTP 오류! 상태: ${response.status}`); | |
94 | + } | |
95 | + | |
96 | + const blob = await response.blob(); | |
97 | + console.log('비디오 Blob 크기:', blob.size); | |
98 | + | |
99 | + // 썸네일 생성 | |
100 | + this.thumbnailUrl = await this.createThumbnail(blob); | |
101 | + } catch (error) { | |
102 | + console.error('썸네일 생성 실패:', error); | |
103 | + this.error = true; | |
104 | + } | |
105 | + } | |
106 | +} | |
107 | +</script> | |
108 | +<style scoped> | |
109 | +.loading, | |
110 | +.error { | |
111 | + display: flex; | |
112 | + align-items: center; | |
113 | + justify-content: center; | |
114 | + width: 100%; | |
115 | + height: 100%; | |
116 | + min-height: 180px; | |
117 | + background-color: #f0f0f0; | |
118 | +} | |
119 | + | |
120 | +.loading { | |
121 | + color: #666; | |
122 | +} | |
123 | + | |
124 | +.error { | |
125 | + color: #e53935; | |
126 | +} | |
127 | +</style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/component/listLayout/CardStyleComponent.vue
+++ client/views/component/listLayout/CardStyleComponent.vue
... | ... | @@ -3,8 +3,9 @@ |
3 | 3 |
<li v-for="(item, idx) in list" :key="idx" class="mb-30" @click="fnMoveTo(item)"> |
4 | 4 |
<div class="result-box"> |
5 | 5 |
<div class="main-img"> |
6 |
- <img v-if="item.hasOwnProperty('files') && item.files.length > 0" :src="item.files[0].filePath" :alt="item.sj + ' 첫 번째 이미지'"> |
|
6 |
+ <video-thumbnail v-if="name === 'V'" :file-path="item.files[0].filePath" :alt="item.sj + ' 영상 썸네일'" /> |
|
7 | 7 |
<img v-else-if="name === 'M'" :src="getYouTubeThumbnail(item.link)" alt="영상 썸네일"> |
8 |
+ <img v-else-if="item.hasOwnProperty('files') && item.files.length > 0" :src="item.files[0].filePath" :alt="item.sj + ' 첫 번째 이미지'"> |
|
8 | 9 |
<img v-else src="client/resources/images/img6.png" alt="Not found image"> |
9 | 10 |
</div> |
10 | 11 |
<div class="text-box"> |
... | ... | @@ -30,10 +31,15 @@ |
30 | 31 |
</template> |
31 | 32 |
<script> |
32 | 33 |
import { getYouTubeThumbnail } from '../../../resources/js/youtubeUtils'; |
34 |
+import VideoThumbnail from '../VideoThumbnail.vue'; |
|
33 | 35 |
|
34 | 36 |
export default { |
35 | 37 |
name: "CardStyleComponent", |
36 | 38 |
|
39 |
+ components: { |
|
40 |
+ VideoThumbnail, |
|
41 |
+ }, |
|
42 |
+ |
|
37 | 43 |
props: { |
38 | 44 |
name: { |
39 | 45 |
type: String, |
--- client/views/pages/main/Main.vue
+++ client/views/pages/main/Main.vue
... | ... | @@ -43,7 +43,7 @@ |
43 | 43 |
<div class="board w1500"> |
44 | 44 |
<ul> |
45 | 45 |
<template v-for="(item, idx) of icons" :key="idx"> |
46 |
- <li> |
|
46 |
+ <li @click="fnMoveTo(item.routeName)"> |
|
47 | 47 |
<div class="labeling"><img :src="item.url" :alt="item.name + '아이콘'"><span>{{ item.name }}</span></div> |
48 | 48 |
<div :class="{ 'count': true, 'all': item.id === 'TOTAL' }">{{ item.rowCo }}</div> |
49 | 49 |
</li> |
... | ... | @@ -70,7 +70,8 @@ |
70 | 70 |
<div class="new-pic"> |
71 | 71 |
<div v-for="(item, idx2) in tabContent.list" :key="idx2" class="box-wrap"> |
72 | 72 |
<div class="box" @click="fnMoveTo(tabContent.view, item.dcryId)"> |
73 |
- <img :src="item.hasOwnProperty('files') && item.files.length > 0 ? item.files[0].filePath : null" :alt="item.sj" class="tab-image" /> |
|
73 |
+ <img v-if="tabContent.id === 'newPhoto'" :src="item.files[0].filePath" :alt="item.sj" class="tab-image" /> |
|
74 |
+ <video-thumbnail v-if="tabContent.id === 'newVideo'" :file-path="item.files[0].filePath" :alt="item.sj" class="tab-image" /> |
|
74 | 75 |
<div class="info"> |
75 | 76 |
<p>{{ item.sj }}</p> |
76 | 77 |
<span>{{ $dotFormatDate(item.rgsde) }}</span> |
... | ... | @@ -155,9 +156,8 @@ |
155 | 156 |
|
156 | 157 |
// 메인화면 조회 |
157 | 158 |
import { findAllSttusesProc } from "../../../resources/api/main"; |
158 |
- |
|
159 |
-// 유투브 유틸 |
|
160 | 159 |
import { getYouTubeThumbnail } from '../../../resources/js/youtubeUtils'; |
160 |
+import VideoThumbnail from '../../component/VideoThumbnail.vue'; |
|
161 | 161 |
|
162 | 162 |
export default { |
163 | 163 |
components: { |
... | ... | @@ -165,6 +165,7 @@ |
165 | 165 |
SwiperSlide, |
166 | 166 |
PauseOutlined, |
167 | 167 |
CaretRightOutlined, |
168 |
+ VideoThumbnail, |
|
168 | 169 |
}, |
169 | 170 |
setup() { |
170 | 171 |
return { |
... | ... | @@ -203,26 +204,31 @@ |
203 | 204 |
id: "TOTAL", |
204 | 205 |
name: "전체", |
205 | 206 |
url: 'client/resources/images/icon/icon1.png', |
207 |
+ routeName: 'TotalSearch', |
|
206 | 208 |
}, |
207 | 209 |
{ |
208 | 210 |
id: "dcry_photo", |
209 | 211 |
name: "사진", |
210 | 212 |
url: 'client/resources/images/icon/icon2.png', |
213 |
+ routeName: 'PicHistorySearch', |
|
211 | 214 |
}, |
212 | 215 |
{ |
213 | 216 |
id: "dcry_vido", |
214 | 217 |
name: "영상", |
215 | 218 |
url: 'client/resources/images/icon/icon3.png', |
219 |
+ routeName: 'VideoHistorySearch', |
|
216 | 220 |
}, |
217 | 221 |
{ |
218 | 222 |
id: "media_vido", |
219 | 223 |
name: "미디어", |
220 | 224 |
url: 'client/resources/images/icon/icon4.png', |
225 |
+ routeName: 'MediaVideoSearch', |
|
221 | 226 |
}, |
222 | 227 |
{ |
223 | 228 |
id: "nes_dta", |
224 | 229 |
name: "보도자료", |
225 | 230 |
url: 'client/resources/images/icon/icon5.png', |
231 |
+ routeName: 'NewsReleaseSearch', |
|
226 | 232 |
}, |
227 | 233 |
], |
228 | 234 |
slides: [ |
... | ... | @@ -331,7 +337,7 @@ |
331 | 337 |
this.$router.push({ name: page, query: { id: id } }); |
332 | 338 |
} |
333 | 339 |
} |
334 |
- } |
|
340 |
+ }, |
|
335 | 341 |
}, |
336 | 342 |
}; |
337 | 343 |
</script>(파일 끝에 줄바꿈 문자 없음) |
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?