
--- client/views/component/VideoThumbnail.vue
... | ... | @@ -1,126 +0,0 @@ |
1 | -<template> | |
2 | - <img v-if="thumbnailUrl" :src="thumbnailUrl" alt="비디오 썸네일"> | |
3 | - <div v-else-if="error" class="error">썸네일 생성 실패</div> | |
4 | - <div v-else class="loading">로딩 중...</div> | |
5 | -</template> | |
6 | -<script> | |
7 | -export default { | |
8 | - data() { | |
9 | - return { | |
10 | - thumbnailUrl: null, | |
11 | - error: false | |
12 | - } | |
13 | - }, | |
14 | - props: { | |
15 | - filePath: String | |
16 | - }, | |
17 | - methods: { | |
18 | - createThumbnail(videoBlob) { | |
19 | - return new Promise((resolve, reject) => { | |
20 | - // 동영상 요소 생성 | |
21 | - const video = document.createElement('video'); | |
22 | - video.style.display = 'none'; | |
23 | - document.body.appendChild(video); | |
24 | - | |
25 | - // 캔버스 요소 생성 | |
26 | - const canvas = document.createElement('canvas'); | |
27 | - canvas.width = 320; | |
28 | - canvas.height = 180; | |
29 | - | |
30 | - // 동영상 파일로부터 URL 생성 | |
31 | - const videoUrl = URL.createObjectURL(videoBlob); | |
32 | - video.src = videoUrl; | |
33 | - | |
34 | - // 로드 시간 제한 설정 (10초) | |
35 | - const timeout = setTimeout(() => { | |
36 | - URL.revokeObjectURL(videoUrl); | |
37 | - document.body.removeChild(video); | |
38 | - reject(new Error('비디오 로드 시간 초과')); | |
39 | - }, 10000); | |
40 | - | |
41 | - // 오류 처리 | |
42 | - video.onerror = (e) => { | |
43 | - clearTimeout(timeout); | |
44 | - URL.revokeObjectURL(videoUrl); | |
45 | - document.body.removeChild(video); | |
46 | - reject(new Error('비디오 로드 실패: ' + e.message)); | |
47 | - }; | |
48 | - | |
49 | - // 메타데이터 로드 후 처리 | |
50 | - video.onloadedmetadata = () => { | |
51 | - // 첫 프레임이나 특정 시점으로 이동 | |
52 | - video.currentTime = 0.5; | |
53 | - }; | |
54 | - | |
55 | - // 특정 시간으로 이동 완료 후 썸네일 생성 | |
56 | - video.onseeked = () => { | |
57 | - try { | |
58 | - clearTimeout(timeout); | |
59 | - | |
60 | - const ctx = canvas.getContext('2d'); | |
61 | - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); | |
62 | - | |
63 | - // 썸네일 URL 생성 | |
64 | - const thumbnailUrl = canvas.toDataURL('image/jpeg', 0.7); | |
65 | - | |
66 | - // 리소스 정리 | |
67 | - URL.revokeObjectURL(videoUrl); | |
68 | - document.body.removeChild(video); | |
69 | - | |
70 | - resolve(thumbnailUrl); | |
71 | - } catch (error) { | |
72 | - URL.revokeObjectURL(videoUrl); | |
73 | - document.body.removeChild(video); | |
74 | - reject(error); | |
75 | - } | |
76 | - }; | |
77 | - }); | |
78 | - } | |
79 | - }, | |
80 | - async mounted() { | |
81 | - try { | |
82 | - console.log('비디오 파일 경로:', this.filePath); | |
83 | - | |
84 | - // 백엔드에서 파일 가져오기 | |
85 | - const response = await fetch(this.filePath, { | |
86 | - // credentials: 'include', // 필요한 경우 쿠키 포함 | |
87 | - // mode: 'cors', // CORS 모드 설정 | |
88 | - }); | |
89 | - | |
90 | - if (!response.ok) { | |
91 | - throw new Error(`HTTP 오류! 상태: ${response.status}`); | |
92 | - } | |
93 | - | |
94 | - const blob = await response.blob(); | |
95 | - console.log('비디오 Blob 크기:', blob.size); | |
96 | - | |
97 | - // 썸네일 생성 | |
98 | - this.thumbnailUrl = await this.createThumbnail(blob); | |
99 | - } catch (error) { | |
100 | - console.error('썸네일 생성 실패:', error); | |
101 | - this.error = true; | |
102 | - } | |
103 | - } | |
104 | -} | |
105 | -</script> | |
106 | -<style scoped> | |
107 | -.loading, | |
108 | -.error { | |
109 | - display: flex; | |
110 | - align-items: center; | |
111 | - justify-content: center; | |
112 | - width: 100%; | |
113 | - height: 100%; | |
114 | - min-height: 180px; | |
115 | - background-color: #f0f0f0; | |
116 | - border-radius: 30px; | |
117 | -} | |
118 | - | |
119 | -.loading { | |
120 | - color: #666; | |
121 | -} | |
122 | - | |
123 | -.error { | |
124 | - color: #e53935; | |
125 | -} | |
126 | -</style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/component/listLayout/CardStyleComponent.vue
+++ client/views/component/listLayout/CardStyleComponent.vue
... | ... | @@ -3,8 +3,7 @@ |
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 |
- <video-thumbnail v-if="name === 'V'" :file-path="item.files[0].filePath" :alt="item.sj + ' 영상 썸네일'" /> |
|
7 |
- <img v-else-if="name === 'M'" :src="getYouTubeThumbnail(item.link)" alt="영상 썸네일"> |
|
6 |
+ <img v-if="name === 'M'" :src="getYouTubeThumbnail(item.link)" alt="영상 썸네일"> |
|
8 | 7 |
<img v-else-if="item.hasOwnProperty('files') && item.files.length > 0" :src="item.files[0].filePath" :alt="item.sj + ' 첫 번째 이미지'"> |
9 | 8 |
<img v-else src="client/resources/images/img6.png" alt="Not found image"> |
10 | 9 |
</div> |
... | ... | @@ -31,14 +30,9 @@ |
31 | 30 |
</template> |
32 | 31 |
<script> |
33 | 32 |
import { getYouTubeThumbnail } from '../../../resources/js/youtubeUtils'; |
34 |
-import VideoThumbnail from '../VideoThumbnail.vue'; |
|
35 | 33 |
|
36 | 34 |
export default { |
37 | 35 |
name: "CardStyleComponent", |
38 |
- |
|
39 |
- components: { |
|
40 |
- VideoThumbnail, |
|
41 |
- }, |
|
42 | 36 |
|
43 | 37 |
props: { |
44 | 38 |
name: { |
--- client/views/pages/bbsDcry/photo/PicHistoryDetail.vue
+++ client/views/pages/bbsDcry/photo/PicHistoryDetail.vue
... | ... | @@ -26,18 +26,19 @@ |
26 | 26 |
<div> |
27 | 27 |
<div class="gallery"> |
28 | 28 |
<div class="main-swiper"> |
29 |
- <swiper :style="{ '--swiper-navigation-color': '#fff', '--swiper-pagination-color': '#fff' }" :loop="true" |
|
30 |
- :spaceBetween="10" :thumbs="{ swiper: thumbsSwiper }" :modules="modules" class="mySwiper2"> |
|
29 |
+ <swiper :style="{ '--swiper-navigation-color': '#fff', '--swiper-pagination-color': '#fff' }" :loop="true" :spaceBetween="10" :thumbs="{ swiper: thumbsSwiper }" :modules="modules" class="mySwiper2"> |
|
31 | 30 |
<swiper-slide v-for="(item, idx) of findResult.files" :key="idx"> |
32 | 31 |
<img :src="item.filePath" :alt="item.fileNm" /> |
33 | 32 |
</swiper-slide> |
34 | 33 |
</swiper> |
35 | 34 |
</div> |
36 | 35 |
<div class="thumbnail"> |
37 |
- <swiper @swiper="setThumbsSwiper" :spaceBetween="20" :slidesPerView="4" :freeMode="true" |
|
38 |
- :watchSlidesProgress="true" :modules="modules" :navigation="true" class="mySwiper"> |
|
36 |
+ <swiper @swiper="setThumbsSwiper" :spaceBetween="20" :slidesPerView="4" :freeMode="true" :watchSlidesProgress="true" :modules="modules" :navigation="true" class="mySwiper"> |
|
39 | 37 |
<swiper-slide v-for="(item, idx) of findResult.files" :key="idx"> |
40 |
- <input type="checkbox" :id="'photo_' + idx" :value="item.fileId" v-model="selectedFiles" /> |
|
38 |
+ <div class="checkbox-wrapper" @click.stop> |
|
39 |
+ <input type="checkbox" :id="'photo_' + idx" :value="item.fileId" v-model="selectedFiles" /> |
|
40 |
+ <label :for="'photo_' + idx"></label> |
|
41 |
+ </div> |
|
41 | 42 |
<img :src="item.filePath" :alt="item.fileNm" /> |
42 | 43 |
</swiper-slide> |
43 | 44 |
</swiper> |
... | ... | @@ -181,9 +182,11 @@ |
181 | 182 |
// 파일 다운로드 |
182 | 183 |
async fnDownload(type) { |
183 | 184 |
// 유효성 검사 |
184 |
- if (type === 'selected' && this.selectedFiles.length === 0) { |
|
185 |
- alert("파일을 1개 이상 선택하거나 전체 다운로드를 클릭해주세요."); |
|
186 |
- return; |
|
185 |
+ if (type === 'selected') { |
|
186 |
+ if (this.selectedFiles.length === 0) { |
|
187 |
+ alert("파일을 1개 이상 선택하거나 전체 다운로드를 클릭해주세요."); |
|
188 |
+ return; |
|
189 |
+ } |
|
187 | 190 |
} |
188 | 191 |
|
189 | 192 |
let url = null; |
... | ... | @@ -191,18 +194,19 @@ |
191 | 194 |
|
192 | 195 |
try { |
193 | 196 |
// 파일 ID 수집 |
194 |
- let fileIds; |
|
197 |
+ let fileList; |
|
195 | 198 |
if (type === 'selected') { |
196 |
- if (this.selectedFiles.length == 1) { |
|
197 |
- fileIds = this.selectedFiles[0]; |
|
199 |
+ fileList = this.selectedFiles; |
|
200 |
+ } else if (type === 'all') { |
|
201 |
+ if (this.findResult.files.length == 1) { |
|
202 |
+ fileList = this.findResult.files[0].fileId; |
|
198 | 203 |
} else { |
199 |
- fileIds = this.selectedFiles.join(','); |
|
204 |
+ fileList = this.findResult.files.map(file => file.fileId); |
|
200 | 205 |
} |
201 |
- } else if (type === 'all' && this.findResult.files.length > 1) { |
|
202 |
- fileIds = this.findResult.files.map(file => file.fileId).join(','); |
|
203 | 206 |
} |
204 | 207 |
|
205 |
- let isMultiple = this.selectedFiles.length > 1; |
|
208 |
+ let isMultiple = fileList.length > 1; |
|
209 |
+ let fileIds = isMultiple ? fileList.join(',') : fileList[0]; |
|
206 | 210 |
const response = isMultiple ? await multiFileDownloadProc(fileIds) : await fileDownloadProc(fileIds); |
207 | 211 |
|
208 | 212 |
// 파일명 조회 |
--- client/views/pages/bbsDcry/video/VideoHistoryDetail.vue
+++ client/views/pages/bbsDcry/video/VideoHistoryDetail.vue
... | ... | @@ -23,8 +23,8 @@ |
23 | 23 |
</dd> |
24 | 24 |
</dl> |
25 | 25 |
<div class="gallery video"> |
26 |
- <img :src="eximg" alt=""> |
|
27 |
- <Video-component :videoUrl="findResult.files[0].filePath" /> |
|
26 |
+ <Video-component v-if="findResult.hasOwnProperty('files') && findResult.files.length > 0" :videoUrl="findResult.files[0].filePath" /> |
|
27 |
+ <img v-else :src="eximg" alt=""> |
|
28 | 28 |
</div> |
29 | 29 |
</form> |
30 | 30 |
<h3>내용</h3> |
--- client/views/pages/main/Main.vue
+++ client/views/pages/main/Main.vue
... | ... | @@ -70,8 +70,7 @@ |
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 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" /> |
|
73 |
+ <img :src="item.files[0].filePath" :alt="item.sj" class="tab-image" /> |
|
75 | 74 |
<div class="info"> |
76 | 75 |
<p>{{ item.sj }}</p> |
77 | 76 |
<span>{{ $dotFormatDate(item.rgsde) }}</span> |
... | ... | @@ -100,7 +99,7 @@ |
100 | 99 |
<router-link :to="{ name: 'MediaVideoSearch' }" class="gopage">더보기</router-link> |
101 | 100 |
</div> |
102 | 101 |
<div class="media-wrap"> |
103 |
- <template v-for="(item, index) of mediaContents" :key="index"> |
|
102 |
+ <template v-for="(item, idx) of mediaContents" :key="idx"> |
|
104 | 103 |
<div class="media-box" @click="fnMoveTo('MediaVideoDetail', item.mediaVidoId)"> |
105 | 104 |
<img :src="item.url" :alt="item.sj" class="media-image" /> |
106 | 105 |
<div class="info"> |
... | ... | @@ -157,7 +156,6 @@ |
157 | 156 |
// 메인화면 조회 |
158 | 157 |
import { findAllSttusesProc } from "../../../resources/api/main"; |
159 | 158 |
import { getYouTubeThumbnail } from '../../../resources/js/youtubeUtils'; |
160 |
-import VideoThumbnail from '../../component/VideoThumbnail.vue'; |
|
161 | 159 |
|
162 | 160 |
export default { |
163 | 161 |
components: { |
... | ... | @@ -165,7 +163,6 @@ |
165 | 163 |
SwiperSlide, |
166 | 164 |
PauseOutlined, |
167 | 165 |
CaretRightOutlined, |
168 |
- VideoThumbnail, |
|
169 | 166 |
}, |
170 | 167 |
setup() { |
171 | 168 |
return { |
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?