
+++ client/resources/api/mediaVido.js
... | ... | @@ -0,0 +1,26 @@ |
1 | +import { apiClient } from "./index"; | |
2 | + | |
3 | +// 미디어영상 목록 조회 | |
4 | +export const findAllMediaVidosProc = (searchReqDTO) => { | |
5 | + return apiClient.get(`/mediaVido/findAllMediaVidos.json`, { params: searchReqDTO }); | |
6 | +} | |
7 | + | |
8 | +// 미디어영상 상세 조회 | |
9 | +export const findMediaVidoProc = (mediaVidoId) => { | |
10 | + return apiClient.get(`/mediaVido/${mediaVidoId}/findMediaVido.json`); | |
11 | +} | |
12 | + | |
13 | +// 미디어영상 등록 | |
14 | +export const saveMediaVidoProc = (mediaVidoInsertDTO) => { | |
15 | + return apiClient.post(`/mediaVido/saveMediaVido.json`, mediaVidoInsertDTO); | |
16 | +} | |
17 | + | |
18 | +// 미디어영상 수정 | |
19 | +export const updateMediaVidoDcry = (mediaVidoUpdateDTO) => { | |
20 | + return apiClient.put(`/mediaVido/updateMediaVido.json`, mediaVidoUpdateDTO); | |
21 | +} | |
22 | + | |
23 | +// 미디어영상 삭제 | |
24 | +export const deleteMediaVidoProc = (mediaVidoId) => { | |
25 | + return apiClient.put(`/mediaVido/${mediaVidoId}/deleteMediaVido.json`); | |
26 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/api/nesDta.js
... | ... | @@ -0,0 +1,26 @@ |
1 | +import { apiClient, fileClient } from "./index"; | |
2 | + | |
3 | +// 보도자료 목록 조회 | |
4 | +export const findAllNesDtasProc = (searchReqDTO) => { | |
5 | + return apiClient.get(`/nesDta/findAllNesDtas.json`, { params: searchReqDTO }); | |
6 | +} | |
7 | + | |
8 | +// 보도자료 상세 조회 | |
9 | +export const findNesDtaProc = (nesDtaId) => { | |
10 | + return apiClient.get(`/nesDta/${nesDtaId}/findNesDta.json`); | |
11 | +} | |
12 | + | |
13 | +// 보도자료 등록 | |
14 | +export const saveNesDtaProc = (formData) => { | |
15 | + return fileClient.post(`/nesDta/saveNesDta.file`, formData); | |
16 | +} | |
17 | + | |
18 | +// 보도자료 수정 | |
19 | +export const updateNesDtaProc = (formData) => { | |
20 | + return fileClient.put(`/nesDta/updateNesDta.file`, formData); | |
21 | +} | |
22 | + | |
23 | +// 보도자료 삭제 | |
24 | +export const deleteNesDtaProc = (nesDtaId) => { | |
25 | + return apiClient.put(`/nesDta/${nesDtaId}/deleteNesDta.json`); | |
26 | +}(파일 끝에 줄바꿈 문자 없음) |
--- client/resources/js/youtubeUtils.js
+++ client/resources/js/youtubeUtils.js
... | ... | @@ -1,5 +1,3 @@ |
1 |
-// youtubeUtils.js |
|
2 |
- |
|
3 | 1 |
/** |
4 | 2 |
* YouTube URL에서 비디오 ID를 추출합니다. |
5 | 3 |
*/ |
--- client/views/component/listLayout/CardStyleComponent.vue
+++ client/views/component/listLayout/CardStyleComponent.vue
... | ... | @@ -4,12 +4,11 @@ |
4 | 4 |
<div class="result-box"> |
5 | 5 |
<div class="main-img"> |
6 | 6 |
<img v-if="item.hasOwnProperty('files') && item.files.length > 0" :src="item.files[0].filePath" :alt="item.sj + ' 첫 번째 이미지'"> |
7 |
+ <img v-else-if="name === 'M'" :src="getYouTubeThumbnail(item.link)" alt="영상 썸네일"> |
|
7 | 8 |
<img v-else src="client/resources/images/img6.png" alt="Not found image"> |
8 | 9 |
</div> |
9 | 10 |
<div class="text-box"> |
10 |
- <router-link :to="{ path: '/PicHistoryDetail.page' }"> |
|
11 |
- <h5>{{ item.sj }}</h5> |
|
12 |
- </router-link> |
|
11 |
+ <h5>{{ item.sj }}</h5> |
|
13 | 12 |
<p v-if="item.hasOwnProperty('adres')" class="address">{{ item.adres }}</p> |
14 | 13 |
<p class="text">{{ $stripHtml(item.cn) }}</p> |
15 | 14 |
<div class="mb-20"> |
... | ... | @@ -30,13 +29,15 @@ |
30 | 29 |
</ul> |
31 | 30 |
</template> |
32 | 31 |
<script> |
32 |
+import { getYouTubeThumbnail } from '../../../resources/js/youtubeUtils'; |
|
33 |
+ |
|
33 | 34 |
export default { |
34 | 35 |
name: "CardStyleComponent", |
35 | 36 |
|
36 | 37 |
props: { |
37 | 38 |
name: { |
38 | 39 |
type: String, |
39 |
- default: 'P', |
|
40 |
+ default: 'P' |
|
40 | 41 |
}, |
41 | 42 |
list: { |
42 | 43 |
type: Array, |
... | ... | @@ -60,6 +61,8 @@ |
60 | 61 |
}, |
61 | 62 |
|
62 | 63 |
methods: { |
64 |
+ // 유투브 썸네일 제작 |
|
65 |
+ getYouTubeThumbnail, |
|
63 | 66 |
// 페이지 이동 |
64 | 67 |
fnMoveTo(item) { |
65 | 68 |
let key = null; |
+++ client/views/component/player/VideoComponent.vue
... | ... | @@ -0,0 +1,0 @@ |
+++ client/views/component/player/YoutubeComponent.vue
... | ... | @@ -0,0 +1,103 @@ |
1 | +<template> | |
2 | + <iframe v-if="videoId && !loadError" width="100%" height="843.75px" :src="embedUrl" frameborder="0" referrerpolicy="strict-origin-when-cross-origin" sandbox="allow-scripts allow-same-origin allow-presentation allow-popups allow-popups-to-escape-sandbox" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen @error="handleError"></iframe> | |
3 | + <div v-else style="text-align: center;"> | |
4 | + <img src="client/resources/images/no_media.png" alt="미디어를 표시할 수 없습니다" style="width: auto; height: auto;" /> | |
5 | + </div> | |
6 | +</template> | |
7 | +<script> | |
8 | +export default { | |
9 | + name: 'YoutubePlayer', | |
10 | + | |
11 | + props: { | |
12 | + link: { | |
13 | + type: String, | |
14 | + default: '' | |
15 | + } | |
16 | + }, | |
17 | + | |
18 | + data() { | |
19 | + return { | |
20 | + loadError: false | |
21 | + } | |
22 | + }, | |
23 | + | |
24 | + computed: { | |
25 | + videoId() { | |
26 | + if (!this.link) { | |
27 | + this.loadError = true; | |
28 | + return null; | |
29 | + } | |
30 | + | |
31 | + try { | |
32 | + let id = null; | |
33 | + | |
34 | + // 일반 YouTube URL (youtube.com/watch?v=VIDEO_ID) | |
35 | + if (this.link.includes('youtube.com/watch')) { | |
36 | + const url = new URL(this.link); | |
37 | + id = url.searchParams.get('v'); | |
38 | + } | |
39 | + | |
40 | + // 단축 URL (youtu.be/VIDEO_ID) | |
41 | + else if (this.link.includes('youtu.be/')) { | |
42 | + const parts = this.link.split('youtu.be/'); | |
43 | + if (parts.length > 1) { | |
44 | + id = parts[1].split('?')[0].split('&')[0]; | |
45 | + } | |
46 | + } | |
47 | + | |
48 | + // 임베드 URL (youtube.com/embed/VIDEO_ID) | |
49 | + else if (this.link.includes('youtube.com/embed/')) { | |
50 | + const parts = this.link.split('youtube.com/embed/'); | |
51 | + if (parts.length > 1) { | |
52 | + id = parts[1].split('?')[0].split('&')[0]; | |
53 | + } | |
54 | + } | |
55 | + | |
56 | + if (!id) { | |
57 | + this.loadError = true; | |
58 | + return null; | |
59 | + } | |
60 | + | |
61 | + return id; | |
62 | + } catch (e) { | |
63 | + console.error('YouTube URL 파싱 오류:', e); | |
64 | + this.loadError = true; | |
65 | + return null; | |
66 | + } | |
67 | + }, | |
68 | + | |
69 | + embedUrl() { | |
70 | + if (!this.videoId) return ''; | |
71 | + return `https://www.youtube-nocookie.com/embed/${this.videoId}?rel=0&modestbranding=1&origin=${encodeURIComponent(window.location.origin)}`; | |
72 | + } | |
73 | + }, | |
74 | + | |
75 | + methods: { | |
76 | + handleError() { | |
77 | + console.error('YouTube 비디오 로드 실패'); | |
78 | + this.loadError = true; | |
79 | + } | |
80 | + }, | |
81 | + | |
82 | + mounted() { | |
83 | + if (this.videoId) { | |
84 | + const checkIframeLoaded = setTimeout(() => { | |
85 | + const iframe = this.$el.querySelector('iframe'); | |
86 | + if (iframe) { | |
87 | + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; | |
88 | + if (!iframeDoc) { | |
89 | + this.loadError = true; | |
90 | + } | |
91 | + } | |
92 | + clearTimeout(checkIframeLoaded); | |
93 | + }, 5000); // 5초 타임아웃 | |
94 | + } | |
95 | + }, | |
96 | + | |
97 | + watch: { | |
98 | + link() { | |
99 | + this.loadError = false; | |
100 | + } | |
101 | + } | |
102 | +} | |
103 | +</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/user/MediaVideoDetail.vue
+++ client/views/pages/user/MediaVideoDetail.vue
... | ... | @@ -1,148 +1,201 @@ |
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 class="gallery video"> |
|
28 |
- <img :src="eximg" alt=""> |
|
29 |
- </div> |
|
30 |
- </form> |
|
31 |
- |
|
32 |
- <h3>내용</h3> |
|
33 |
- <form action="" class=" info-form mb-50"> |
|
34 |
- <dl> |
|
35 |
- <dd> |
|
36 |
- <p> 대한민국 최대의 내륙 산업단지를 보유하고, 서울로부터 277km, 부산으로부터 167km 거리에 있으며, 면적은 615㎢로 경상북도 전체 면적의 3.2%에 달합니다. 인구는 |
|
37 |
- 41만 명이고, 선산읍, 고아읍, 산동읍을 비롯한 3읍, 5면, 17개 동으로 구성되어 있습니다.</p> |
|
38 |
- |
|
39 |
- </dd> |
|
40 |
- </dl> |
|
41 |
- </form> |
|
42 |
- |
|
43 |
- <h3>기본정보</h3> |
|
44 |
- <form action="" class="info-form mb-50"> |
|
45 |
- <dl> |
|
46 |
- <dd class="mb-20"> |
|
47 |
- <img :src="addressicon" alt=""> |
|
48 |
- <span>원본주소</span> |
|
49 |
- <p>https://youtu.be/2F2gWkEnSz4</p> |
|
50 |
- </dd> |
|
51 |
- <dd class="mb-20"> |
|
52 |
- <img :src="yearicon" alt=""> |
|
53 |
- <span>생산연도</span> |
|
54 |
- <p>2017</p> |
|
55 |
- |
|
56 |
- </dd> |
|
57 |
- <dd> |
|
58 |
- <img :src="categoryicon" alt=""> |
|
59 |
- <span>카테고리</span> |
|
60 |
- <ul class="category"> |
|
61 |
- <li v-if="resultitem.category1" class="category1">카테고리1</li> |
|
62 |
- <li v-if="resultitem.category2" class="category2">카테고리2</li> |
|
63 |
- </ul> |
|
64 |
- |
|
65 |
- </dd> |
|
66 |
- |
|
67 |
- </dl> |
|
68 |
- </form> |
|
69 |
- <div class="btn-group flex-center"> |
|
70 |
- <button class="red-line " type="button" @click="fnDeleteUser">삭제</button> |
|
71 |
- <button class="blue-line " type="button" @click="fnUpdateUser">수정</button> |
|
72 |
- <button class="gray-line-bg " type="button" @click="fnUpdateUser">목록</button> |
|
73 |
- </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> |
|
74 | 14 |
</div> |
15 |
+ <div class="gallery-form mb-40"> |
|
16 |
+ <dl class="mb-20"> |
|
17 |
+ <dd> |
|
18 |
+ <p>{{ mediaVido.sj }} </p> |
|
19 |
+ <div class="date flex align-center"> |
|
20 |
+ <img :src="calendaricon" alt=""> |
|
21 |
+ <span>{{ $dotFormatDate(mediaVido.rgsde) }}</span> |
|
22 |
+ </div> |
|
23 |
+ </dd> |
|
24 |
+ </dl> |
|
25 |
+ <div class="gallery video"> |
|
26 |
+ <Youtube-component :link="mediaVido.link" /> |
|
27 |
+ </div> |
|
28 |
+ </div> |
|
29 |
+ <h3>내용</h3> |
|
30 |
+ <div class="info-form mb-50"> |
|
31 |
+ <dl> |
|
32 |
+ <dd> |
|
33 |
+ <Viewer-component :content="mediaVido.cn" /> |
|
34 |
+ </dd> |
|
35 |
+ </dl> |
|
36 |
+ </div> |
|
37 |
+ <h3>기본정보</h3> |
|
38 |
+ <div class="info-form mb-50"> |
|
39 |
+ <dl> |
|
40 |
+ <dd class="mb-20"> |
|
41 |
+ <img :src="addressicon" alt=""> |
|
42 |
+ <span>원본주소</span> |
|
43 |
+ <p>{{ mediaVido.link }}</p> |
|
44 |
+ </dd> |
|
45 |
+ <dd class="mb-20"> |
|
46 |
+ <img :src="yearicon" alt=""> |
|
47 |
+ <span>생산연도</span> |
|
48 |
+ <p>{{ $dotFormatDate(mediaVido.prdctnYear) }}</p> |
|
49 |
+ </dd> |
|
50 |
+ <dd> |
|
51 |
+ <img :src="categoryicon" alt=""> |
|
52 |
+ <span>카테고리</span> |
|
53 |
+ <ul class="category"> |
|
54 |
+ <li v-for="(item, idx) of mediaVido.ctgrys" :key="idx" class="category">{{ item.ctgryNm }}</li> |
|
55 |
+ </ul> |
|
56 |
+ </dd> |
|
57 |
+ </dl> |
|
58 |
+ </div> |
|
59 |
+ <div class="btn-group flex-center"> |
|
60 |
+ <button class="red-line " type="button" @click="fnDelete">삭제</button> |
|
61 |
+ <button class="blue-line " type="button" @click="fnMoveTo('MediaVideoInsert', pageId)">수정</button> |
|
62 |
+ <button class="gray-line-bg " type="button" @click="fnMoveTo('MediaVideoSearch')">목록</button> |
|
63 |
+ </div> |
|
64 |
+ </div> |
|
75 | 65 |
</template> |
76 |
- |
|
77 | 66 |
<script> |
78 |
-import axios from "axios"; |
|
79 |
-import { ref } from 'vue'; |
|
80 |
-import { updateUsers, logOutProc, updatePassword } from "../../../resources/api/user" |
|
81 |
-// Import Swiper Vue components |
|
82 |
-import { CaretRightOutlined, PauseOutlined } from '@ant-design/icons-vue'; |
|
83 |
-import { Swiper, SwiperSlide } from 'swiper/vue'; |
|
84 |
- |
|
85 |
-// Import Swiper styles |
|
86 |
-import 'swiper/css'; |
|
87 |
- |
|
88 |
-import 'swiper/css/free-mode'; |
|
89 |
-import 'swiper/css/navigation'; |
|
90 |
-import 'swiper/css/thumbs'; |
|
91 |
- |
|
92 |
-// import required modules |
|
93 |
-import { FreeMode, Navigation, Thumbs } from 'swiper/modules'; |
|
67 |
+// COMPONENT |
|
68 |
+import YoutubeComponent from '../../component/player/YoutubeComponent.vue'; |
|
69 |
+import ViewerComponent from '../../component/editor/ViewerComponent.vue'; |
|
70 |
+// API |
|
71 |
+import { findMediaVidoProc, deleteMediaVidoProc } from '@/resources/api/mediaVido'; |
|
72 |
+import { fileDownloadProc } from '@/resources/api/file'; |
|
94 | 73 |
|
95 | 74 |
export default { |
96 |
- components: { |
|
97 |
- PauseOutlined, |
|
98 |
- CaretRightOutlined, |
|
99 |
- Swiper, |
|
100 |
- SwiperSlide, |
|
101 |
- }, |
|
102 |
- setup() { |
|
103 |
- const thumbsSwiper = ref(null); |
|
75 |
+ components: { |
|
76 |
+ YoutubeComponent, |
|
77 |
+ ViewerComponent, |
|
78 |
+ }, |
|
104 | 79 |
|
105 |
- const setThumbsSwiper = (swiper) => { |
|
106 |
- thumbsSwiper.value = swiper; |
|
107 |
- }; |
|
80 |
+ data() { |
|
81 |
+ return { |
|
82 |
+ // ICON |
|
83 |
+ calendaricon: 'client/resources/images/icon/calendaricon.png', |
|
84 |
+ homeicon: 'client/resources/images/icon/home.png', |
|
85 |
+ erroricon: 'client/resources/images/icon/error.png', |
|
86 |
+ righticon: 'client/resources/images/icon/right.png', |
|
87 |
+ addressicon: 'client/resources/images/icon/addressicon.png', |
|
88 |
+ yearicon: 'client/resources/images/icon/yearicon.png', |
|
89 |
+ categoryicon: 'client/resources/images/icon/categoryicon.png', |
|
108 | 90 |
|
109 |
- return { |
|
110 |
- thumbsSwiper, |
|
111 |
- setThumbsSwiper, |
|
112 |
- modules: [FreeMode, Navigation, Thumbs], |
|
113 |
- }; |
|
114 |
- }, |
|
115 |
- data() { |
|
116 |
- return { |
|
117 |
- resultitem: { |
|
118 |
- category1: true, |
|
119 |
- category2: true, |
|
120 |
- }, |
|
121 |
- calendaricon: 'client/resources/images/icon/calendaricon.png', |
|
122 |
- homeicon: 'client/resources/images/icon/home.png', |
|
123 |
- erroricon: 'client/resources/images/icon/error.png', |
|
124 |
- righticon: 'client/resources/images/icon/right.png', |
|
125 |
- addressicon: 'client/resources/images/icon/addressicon.png', |
|
126 |
- yearicon: 'client/resources/images/icon/yearicon.png', |
|
127 |
- categoryicon: 'client/resources/images/icon/categoryicon.png', |
|
128 |
- eximg: 'client/resources/images/img8.png', |
|
129 |
- slides: [ |
|
130 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 1' }, |
|
131 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 2' }, |
|
132 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 3' }, |
|
133 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 3' }, |
|
134 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 3' }, |
|
135 |
- { img: 'client/resources/images/visual.png', alt: 'Slide 3' }, |
|
136 |
- // Add more slides as needed |
|
137 |
- ], |
|
91 |
+ pageId: null, |
|
92 |
+ mediaVido: {}, |
|
93 |
+ selectedFiles: [], |
|
94 |
+ }; |
|
95 |
+ }, |
|
138 | 96 |
|
139 |
- }; |
|
97 |
+ created() { |
|
98 |
+ this.pageId = this.$route.query.id; |
|
99 |
+ if (this.pageId === null) { |
|
100 |
+ alert("게시물 존재하지 않습니다."); |
|
101 |
+ this.fnMoveTo('MediaVideoSearch'); |
|
102 |
+ } |
|
103 |
+ }, |
|
104 |
+ |
|
105 |
+ mounted() { |
|
106 |
+ this.fnFindDcry(); // 상세 조회 |
|
107 |
+ }, |
|
108 |
+ |
|
109 |
+ methods: { |
|
110 |
+ // 상세 조회 |
|
111 |
+ async fnFindDcry() { |
|
112 |
+ try { |
|
113 |
+ const response = await findMediaVidoProc(this.pageId); |
|
114 |
+ this.mediaVido = response.data.data.mediaVido; |
|
115 |
+ } catch (error) { |
|
116 |
+ alert('조회중 오류가 발생했습니다.'); |
|
117 |
+ this.fnMoveTo('MediaVideoSearch'); |
|
118 |
+ |
|
119 |
+ if (error.response) { |
|
120 |
+ alert(error.response.data.message); |
|
121 |
+ } |
|
122 |
+ console.error(error.message); |
|
123 |
+ } |
|
140 | 124 |
}, |
141 |
- methods: { |
|
125 |
+ |
|
126 |
+ // 파일 다운로드 |
|
127 |
+ async fnDownload(type) { |
|
128 |
+ // 유효성 검사 |
|
129 |
+ if (type === 'selected' && this.selectedFiles.length === 0) { |
|
130 |
+ alert("파일을 1개 이상 선택하거나 전체 다운로드를 클릭해주세요."); |
|
131 |
+ return; |
|
132 |
+ } |
|
133 |
+ |
|
134 |
+ let url = null; |
|
135 |
+ let link = null; |
|
136 |
+ |
|
137 |
+ try { |
|
138 |
+ let fileIds = this.selectedFiles[0]; |
|
139 |
+ const response = await fileDownloadProc(fileIds); |
|
140 |
+ |
|
141 |
+ // 파일명 조회 |
|
142 |
+ let filename = 'downloadFile.bin'; |
|
143 |
+ const filenameRegex = /file[Nn]ame[^;=\n]*=((['"]).*?\2|[^;\n]*)/; |
|
144 |
+ const matches = filenameRegex.exec(response.headers['content-disposition']); |
|
145 |
+ if (matches != null && matches[1]) { |
|
146 |
+ filename = matches[1].replace(/['"]/g, ''); |
|
147 |
+ } |
|
148 |
+ |
|
149 |
+ // 파일 다운로드 생성 |
|
150 |
+ url = window.URL.createObjectURL(new Blob([response.data])); |
|
151 |
+ link = document.createElement('a'); |
|
152 |
+ link.href = url; |
|
153 |
+ link.setAttribute('download', filename); |
|
154 |
+ document.body.appendChild(link); |
|
155 |
+ link.click(); |
|
156 |
+ } catch (error) { |
|
157 |
+ alert('파일 다운로드 중 오류가 발생했습니다.'); |
|
158 |
+ } finally { |
|
159 |
+ // 리소스 정리 |
|
160 |
+ setTimeout(() => { |
|
161 |
+ if (url) { |
|
162 |
+ window.URL.revokeObjectURL(url); |
|
163 |
+ } |
|
164 |
+ if (link && link.parentNode) { |
|
165 |
+ document.body.removeChild(link); |
|
166 |
+ } |
|
167 |
+ }, 100); |
|
168 |
+ } |
|
142 | 169 |
}, |
143 |
- watch: {}, |
|
144 |
- computed: { |
|
170 |
+ |
|
171 |
+ // 삭제 |
|
172 |
+ async fnDelete() { |
|
173 |
+ let isCheck = confirm("해당 페이지를 삭제하시겠습니까?"); |
|
174 |
+ if (!isCheck) { |
|
175 |
+ return; |
|
176 |
+ } |
|
177 |
+ |
|
178 |
+ try { |
|
179 |
+ const response = await deleteMediaVidoProc(this.pageId); |
|
180 |
+ alert('해당 페이지를 삭제했습니다.'); |
|
181 |
+ |
|
182 |
+ this.fnMoveTo('MediaVideoSearch'); |
|
183 |
+ } catch (error) { |
|
184 |
+ if (error.response) { |
|
185 |
+ alert(error.response.data.message); |
|
186 |
+ } |
|
187 |
+ console.error(error.message); |
|
188 |
+ } |
|
145 | 189 |
}, |
146 |
- mounted() { }, |
|
190 |
+ |
|
191 |
+ // 페이지 이동 |
|
192 |
+ fnMoveTo(page, id) { |
|
193 |
+ if (this.$isEmpty(id)) { |
|
194 |
+ this.$router.push({ name: page }); |
|
195 |
+ } else { |
|
196 |
+ this.$router.push({ name: page, query: { id: id } }); |
|
197 |
+ } |
|
198 |
+ }, |
|
199 |
+ }, |
|
147 | 200 |
}; |
148 | 201 |
</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/user/MediaVideoInsert.vue
+++ client/views/pages/user/MediaVideoInsert.vue
... | ... | @@ -12,53 +12,57 @@ |
12 | 12 |
</ul> |
13 | 13 |
</div> |
14 | 14 |
</div> |
15 |
- <form action="" class="insert-form mb-50"> |
|
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="requestDTO.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="requestDTO.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="URL 주소를 입력하세요"></div> |
|
28 |
+ <label for="link">주소</label> |
|
29 |
+ <div class="wfull"><input type="text" id="link" placeholder="URL 주소를 입력하세요" v-model="requestDTO.link"></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="requestDTO.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 |
</dl> |
49 | 48 |
</form> |
50 | 49 |
<div class="btn-group flex-center"> |
51 |
- <button class="cancel">취소</button> |
|
52 |
- <button class="register">등록</button> |
|
50 |
+ <button type="button" class="cancel" @click="fnMoveTo('MediaVideoSearch')">취소</button> |
|
51 |
+ <button type="button" class="register" @click="submitForm"> |
|
52 |
+ <span v-if="$isEmpty(pageId)">등록</span> |
|
53 |
+ <span v-else>수정</span> |
|
54 |
+ </button> |
|
53 | 55 |
</div> |
54 | 56 |
</div> |
55 |
- <CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" /> |
|
57 |
+ <CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" @addCtgries="fnAddCtgries" /> |
|
56 | 58 |
</template> |
57 | 59 |
<script> |
58 | 60 |
import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'; |
59 | 61 |
// COMPONENT |
60 | 62 |
import EditorComponent from '../../component/editor/EditorComponent.vue'; |
61 | 63 |
import CategorySelectModal from '../../component/modal/CategorySelectModal.vue'; |
64 |
+// API |
|
65 |
+import { findMediaVidoProc, saveMediaVidoProc, updateMediaVidoDcry } from '@/resources/api/mediaVido'; |
|
62 | 66 |
|
63 | 67 |
export default { |
64 | 68 |
components: { |
... | ... | @@ -66,116 +70,126 @@ |
66 | 70 |
LeftOutlined, |
67 | 71 |
RightOutlined, |
68 | 72 |
DoubleRightOutlined, |
69 |
- EditorComponent, CategorySelectModal, |
|
73 |
+ EditorComponent, |
|
74 |
+ CategorySelectModal, |
|
70 | 75 |
}, |
71 | 76 |
|
72 | 77 |
data() { |
73 | 78 |
return { |
74 |
- // Define the image sources |
|
79 |
+ // 아이콘 경로 |
|
75 | 80 |
homeicon: 'client/resources/images/icon/home.png', |
76 | 81 |
erroricon: 'client/resources/images/icon/error.png', |
77 | 82 |
righticon: 'client/resources/images/icon/right.png', |
78 |
- fileicon: 'client/resources/images/icon/file.png', |
|
79 | 83 |
searchicon: 'client/resources/images/icon/search.png', |
80 | 84 |
|
81 |
- isModalOpen: false, |
|
85 |
+ pageId: null, |
|
82 | 86 |
|
83 |
- items: [ |
|
84 |
- { id: 1, category: '카테고리 1', selected: false }, |
|
85 |
- { id: 2, category: '카테고리 2', selected: false }, |
|
86 |
- { id: 3, category: '카테고리 3', selected: false }, |
|
87 |
- ], |
|
88 |
- fileNames: [], |
|
89 |
- insertDTO: { |
|
90 |
- sj: null, //제목 |
|
91 |
- cn: null, //내용 |
|
92 |
- adres: null, // 주소 |
|
87 |
+ isModalOpen: false, |
|
88 |
+ isDragging: false, |
|
89 |
+ |
|
90 |
+ // 등록/수정 요청 객체 |
|
91 |
+ requestDTO: { |
|
92 |
+ mediaVidoId: null, // 미디어영상 아이디 |
|
93 |
+ sj: null, // 제목 |
|
94 |
+ cn: null, // 내용 |
|
95 |
+ link: null, // 주소 |
|
93 | 96 |
prdctnYear: null, // 생산연도 |
94 |
- ty: 'P', // 타입 ( P: 사진, V: 영상 ) |
|
95 |
- multipartFiles: null, // 첨부파일 정보 |
|
96 |
- ctgryIds: null, // 카테고리 정보 |
|
97 |
+ ctgryIds: [], // 카테고리 정보 |
|
97 | 98 |
}, |
98 | 99 |
|
99 |
- files: [], |
|
100 | 100 |
selectedCtgries: [], // 카테고리 목록 |
101 | 101 |
}; |
102 | 102 |
}, |
103 |
- computed: { |
|
104 |
- filteredItems() { |
|
105 |
- // This could be modified to support filtering based on searchQuery |
|
106 |
- return this.items.filter(item => |
|
107 |
- item.category.includes(this.searchQuery) |
|
108 |
- ); |
|
103 |
+ |
|
104 |
+ created() { |
|
105 |
+ this.pageId = this.$route.query.id; |
|
106 |
+ if (!this.$isEmpty(this.pageId)) { |
|
107 |
+ this.fnFindMediaVido(); // 상세 조회 |
|
109 | 108 |
} |
110 | 109 |
}, |
111 |
- created() { |
|
112 |
- }, |
|
110 |
+ |
|
113 | 111 |
methods: { |
114 |
- registerCategories() { |
|
115 |
- // Add selected categories to the displayed list |
|
116 |
- this.selectedCtgries = this.items |
|
117 |
- .filter(item => item.selected) |
|
118 |
- .map(item => item.category); |
|
119 |
- this.closeModal(); // Close modal after registration |
|
120 |
- }, |
|
121 |
- removeCategory(index) { |
|
122 |
- // Remove category from the list |
|
123 |
- this.selectedCtgries.splice(index, 1); |
|
124 |
- }, |
|
125 |
- searchCategories() { |
|
126 |
- // You can implement search logic if needed |
|
127 |
- }, |
|
128 |
- nextPage() { |
|
129 |
- if (this.currentPage < this.totalPages) { |
|
130 |
- this.currentPage++; |
|
112 |
+ // 상세 조회 |
|
113 |
+ async fnFindMediaVido() { |
|
114 |
+ try { |
|
115 |
+ const response = await findMediaVidoProc(this.pageId); |
|
116 |
+ this.copyToMediaVidoReqDTO(response.data.data.mediaVido); |
|
117 |
+ } catch (error) { |
|
118 |
+ alert('조회중 오류가 발생했습니다.'); |
|
119 |
+ this.fnMoveTo('MediaVideoSearch'); // 목록으로 이동 |
|
120 |
+ |
|
121 |
+ if (error.response) { |
|
122 |
+ alert(error.response.data.message); |
|
123 |
+ } |
|
124 |
+ console.error(error.message); |
|
131 | 125 |
} |
132 | 126 |
}, |
133 |
- previousPage() { |
|
134 |
- if (this.currentPage > 1) { |
|
135 |
- this.currentPage--; |
|
136 |
- } |
|
127 |
+ |
|
128 |
+ // mediaVido > requestDTO |
|
129 |
+ copyToMediaVidoReqDTO(mediaVido) { |
|
130 |
+ const copyFields = Object.keys(this.requestDTO); |
|
131 |
+ copyFields.forEach(field => { |
|
132 |
+ this.requestDTO[field] = this.$isEmpty(mediaVido[field]) ? null : mediaVido[field]; |
|
133 |
+ }); |
|
134 |
+ this.selectedCtgries = mediaVido.ctgrys.length > 0 ? mediaVido.ctgrys : []; |
|
137 | 135 |
}, |
138 |
- showFileNames(event) { |
|
139 |
- const files = event.target.files; |
|
140 |
- this.fileNames = []; // Clear previous file names |
|
141 | 136 |
|
142 |
- for (let i = 0; i < files.length; i++) { |
|
143 |
- const file = files[i]; |
|
144 |
- const fileType = file.name.split('.').pop().toLowerCase(); // Get file extension |
|
137 |
+ // 카테고리 모달 열기/닫기 |
|
138 |
+ fnToggleModal() { |
|
139 |
+ this.isModalOpen = !this.isModalOpen; |
|
140 |
+ }, |
|
145 | 141 |
|
146 |
- // Set default icon |
|
147 |
- let iconPath = this.fileicons; |
|
142 |
+ // 카테고리 등록 |
|
143 |
+ fnAddCtgries(selectedCtgries) { |
|
144 |
+ this.selectedCtgries = [...this.selectedCtgries, ...selectedCtgries]; |
|
148 | 145 |
|
149 |
- // Determine the icon based on file type |
|
150 |
- if (['jpg', 'jpeg', 'png', 'gif'].includes(fileType)) { |
|
151 |
- iconPath = 'client/resources/images/icon/imgicon.png'; // Example for image files |
|
152 |
- } else if (['pdf'].includes(fileType)) { |
|
153 |
- iconPath = 'client/resources/images/icon/pdficon.png'; // Example for PDF files |
|
154 |
- } else if (['xls'].includes(fileType)) { |
|
155 |
- iconPath = 'client/resources/images/icon/excelicon.png'; // Example for audio files |
|
156 |
- } else if (['hwp'].includes(fileType)) { |
|
157 |
- iconPath = 'client/resources/images/icon/hwpicon.png'; // Example for video files |
|
146 |
+ this.fnToggleModal(); // 카테고리 모달 닫기 |
|
147 |
+ }, |
|
148 |
+ |
|
149 |
+ // 카테고리 삭제 |
|
150 |
+ fnDelCtgry(id) { |
|
151 |
+ this.selectedCtgries = this.selectedCtgries.filter(item => item.ctgryId !== id); |
|
152 |
+ }, |
|
153 |
+ |
|
154 |
+ // 등록 |
|
155 |
+ async submitForm() { |
|
156 |
+ // 유효성 검사 |
|
157 |
+ if (!this.requestDTO.sj) { |
|
158 |
+ alert("제목을 입력해 주세요."); |
|
159 |
+ return; |
|
160 |
+ } |
|
161 |
+ |
|
162 |
+ try { |
|
163 |
+ if (this.$isEmpty(this.pageId)) { |
|
164 |
+ delete this.requestDTO.mediaVidoId; |
|
158 | 165 |
} |
159 | 166 |
|
160 |
- // Push the file name and corresponding icon to the fileNames array |
|
161 |
- this.fileNames.push({ |
|
162 |
- name: file.name, |
|
163 |
- icon: iconPath |
|
164 |
- }); |
|
167 |
+ // requestDTO에 카테고리 Id 추가 |
|
168 |
+ this.requestDTO.ctgryIds = this.selectedCtgries.map(ctgry => ctgry.ctgryId); |
|
169 |
+ |
|
170 |
+ // API 통신 |
|
171 |
+ const response = this.$isEmpty(this.pageId) ? await saveMediaVidoProc(this.requestDTO) : await updateMediaVidoDcry(this.requestDTO); |
|
172 |
+ alert(this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다."); |
|
173 |
+ |
|
174 |
+ this.fnMoveTo('MediaVideoDetail', response.data.data.mediaVidoId); // 상세 페이지로 이동 |
|
175 |
+ } catch (error) { |
|
176 |
+ if (error.response) { |
|
177 |
+ alert(error.response.data.message); |
|
178 |
+ } else { |
|
179 |
+ alert("에러가 발생했습니다."); |
|
180 |
+ } |
|
181 |
+ console.error(error.message); |
|
182 |
+ }; |
|
183 |
+ }, |
|
184 |
+ |
|
185 |
+ // 페이지 이동 |
|
186 |
+ fnMoveTo(page, id) { |
|
187 |
+ if (this.$isEmpty(id)) { |
|
188 |
+ this.$router.push({ name: page }); |
|
189 |
+ } else { |
|
190 |
+ this.$router.push({ name: page, query: { id: id } }); |
|
165 | 191 |
} |
166 |
- }, |
|
167 |
- removeFile(index) { |
|
168 |
- // Remove file from the list |
|
169 |
- this.fileNames.splice(index, 1); |
|
170 |
- console.log(removeFile) |
|
171 |
- }, |
|
172 |
- openModal() { |
|
173 |
- this.isModalOpen = true; |
|
174 |
- }, |
|
175 |
- // 모달 닫기 |
|
176 |
- closeModal() { |
|
177 |
- this.isModalOpen = false; |
|
178 |
- }, |
|
192 |
+ } |
|
179 | 193 |
} |
180 | 194 |
}; |
181 |
-</script> |
|
195 |
+</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/user/MediaVideoSearch.vue
+++ client/views/pages/user/MediaVideoSearch.vue
... | ... | @@ -1,414 +1,284 @@ |
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 |
- <!-- Bind the image source dynamically for homeicon --> |
|
8 |
- <li><img :src="homeicon" alt="Home Icon"> |
|
9 |
- <p>언론에서 바라본 구미시</p> |
|
10 |
- </li> |
|
11 |
- <li><img :src="righticon" alt=""></li> |
|
12 |
- <li>미디어 영상</li> |
|
13 |
- </ul> |
|
14 |
- </div> |
|
15 |
- </div> |
|
16 |
- <div action="search" class="search-form form "> |
|
17 |
- <dl> |
|
18 |
- <dd class="mb-15"> |
|
19 |
- <p>검색범위</p> |
|
20 |
- <ul> |
|
21 |
- <li> |
|
22 |
- <input type="checkbox" id="allScope" v-model="isChkAllScope" |
|
23 |
- @change="fnChkAllOptions('scope')" /> |
|
24 |
- <label for="allScope">전체</label> |
|
25 |
- </li> |
|
26 |
- <li v-for="(scope, idx) in searchType" :key="idx"> |
|
27 |
- <input type="checkbox" :id="idx" :name="searchType" :value="scope.key" |
|
28 |
- v-model="searchReqDTO.searchType" @change="fnChkOption('scope')" /> |
|
29 |
- <label :for="idx">{{ scope.value }}</label> |
|
30 |
- </li> |
|
31 |
- </ul> |
|
32 |
- </dd> |
|
33 |
- <dd class="mb-15"> |
|
34 |
- <p>검색어</p> |
|
35 |
- <div class="wfull"><input type="text" v-model="searchReqDTO.searchText"></div> |
|
36 |
- </dd> |
|
37 |
- <dd class="mb-15"> |
|
38 |
- <p>생산연도</p> |
|
39 |
- <input type="date" v-model="searchReqDTO.startYear"> |
|
40 |
- <p class="mark">~</p> |
|
41 |
- <input type="date" v-model="searchReqDTO.endYear"> |
|
42 |
- </dd> |
|
43 |
- <dd class="mb-20 category-dd"> |
|
44 |
- <p>카테고리</p> |
|
45 |
- <ul> |
|
46 |
- <li v-for="(category, idx) of categorys" :key="idx"> |
|
47 |
- <input type="checkbox" :id="category.ctgryId" name="categorys" :value="category.ctgryId" |
|
48 |
- v-model="searchReqDTO.searchCtgry" /> |
|
49 |
- <label :for="category.ctgryId">{{ category.ctgryNm }}</label> |
|
50 |
- </li> |
|
51 |
- </ul> |
|
52 |
- </dd> |
|
53 |
- <dd class="mb-15"> |
|
54 |
- <p>정렬</p> |
|
55 |
- <ul> |
|
56 |
- <li v-for="(order, idx) of orders" :key="idx"> |
|
57 |
- <input type="radio" :id="order.key" name="orders" :value="order.key" |
|
58 |
- v-model="searchReqDTO.order" /> |
|
59 |
- <label :for="order.key">{{ order.value }}</label> |
|
60 |
- </li> |
|
61 |
- </ul> |
|
62 |
- </dd> |
|
63 |
- <div class="btn-group"> |
|
64 |
- <button class="reset"><img :src="reseticon" alt=""> |
|
65 |
- <p>초기화</p> |
|
66 |
- </button> |
|
67 |
- <button class="search"><img :src="searchicon" alt=""> |
|
68 |
- <p>검색</p> |
|
69 |
- </button> |
|
70 |
- </div> |
|
71 |
- |
|
72 |
- </dl> |
|
73 |
- |
|
74 |
- </div> |
|
75 |
- <div class="search-result"> |
|
76 |
- <div class="tabs"> |
|
77 |
- <div class="flex-sp-bw mb-20 align-center"> |
|
78 |
- <div class="resultext "> |
|
79 |
- <img :src="resulticon" alt=""> |
|
80 |
- <p>총 <b>{{ count }}개</b>의 미디어 영상이 검색되었습니다. </p> |
|
81 |
- </div> |
|
82 |
- <div class="flex "> |
|
83 |
- <ul class="tab-box mb-20"> |
|
84 |
- <li v-for="(tab, index) in tabs" :key="index" class="tab-title" |
|
85 |
- :class="{ active: selectedTab === tab.id }" @click="selectTab(tab.id)"> |
|
86 |
- <img :src="selectedTab === tab.id ? tab.activeImage : tab.inactiveImage" |
|
87 |
- :alt="tab.title" class="tab-icon" /> |
|
88 |
- <p><b>{{ tab.title }}</b></p> |
|
89 |
- </li> |
|
90 |
- </ul> |
|
91 |
- <div class="select-box"> |
|
92 |
- <select v-model="itemsPerPage" @change="changeItemsPerPage"> |
|
93 |
- <option :value="5" selected>5개</option> |
|
94 |
- <option :value="10">10개</option> |
|
95 |
- <option :value="15">15개</option> |
|
96 |
- </select> |
|
97 |
- </div> |
|
98 |
- </div> |
|
99 |
- |
|
100 |
- </div> |
|
101 |
- |
|
102 |
- <div class="tab-content"> |
|
103 |
- <!-- Loop through tabContents, and only display content that matches selectedTab --> |
|
104 |
- <div v-for="(tabContent, idx) in tabContents" :key="idx"> |
|
105 |
- <!-- Display content only if the tab's ID matches the selectedTab --> |
|
106 |
- <div v-show="tabContent.id === selectedTab"> |
|
107 |
- <!-- 카드형 Section (Card Layout) --> |
|
108 |
- <div v-if="tabContent.viewType === 'card'"> |
|
109 |
- <ul class="card-wrap"> |
|
110 |
- <li v-for="(resultitem, index) in paginatedItems" :key="index" class="mb-30"> |
|
111 |
- <div class="result-box"> |
|
112 |
- <!-- Main Image Section --> |
|
113 |
- <div class="main-img"> |
|
114 |
- <img :src="resultitem.img" alt="" class="tab-image" /> |
|
115 |
- </div> |
|
116 |
- <!-- Text Section --> |
|
117 |
- <div class="text-box"> |
|
118 |
- <router-link :to="{ path: '/MediaVideoDetail.page' }"> |
|
119 |
- <h5>{{ resultitem.title }}</h5> |
|
120 |
- </router-link> |
|
121 |
- |
|
122 |
- <p class="address">{{ resultitem.address }}</p> |
|
123 |
- <p class="text">{{ resultitem.content }}</p> |
|
124 |
- |
|
125 |
- <div class="mb-20"> |
|
126 |
- <ul class="category"> |
|
127 |
- <li v-if="resultitem.category1" class="category1">카테고리1</li> |
|
128 |
- <li v-if="resultitem.category2" class="category2">카테고리2</li> |
|
129 |
- </ul> |
|
130 |
- </div> |
|
131 |
- |
|
132 |
- <div class="date"> |
|
133 |
- <ul> |
|
134 |
- <li>생산연도 <b>{{ resultitem.year }}</b></li> |
|
135 |
- <li>|</li> |
|
136 |
- <li>등록 <b>{{ resultitem.date }}</b></li> |
|
137 |
- </ul> |
|
138 |
- </div> |
|
139 |
- </div> |
|
140 |
- </div> |
|
141 |
- </li> |
|
142 |
- </ul> |
|
143 |
- |
|
144 |
- <!-- Empty State if no results in paginatedItems --> |
|
145 |
- <div v-if="paginatedItems.length === 0" class="no-results"> |
|
146 |
- <p>등록된 게시물이 없습니다.</p> |
|
147 |
- </div> |
|
148 |
- </div> |
|
149 |
- |
|
150 |
- <!-- 리스트형 Section (List Layout) --> |
|
151 |
- <div v-if="tabContent.viewType === 'list'"> |
|
152 |
- <ul class="list-wrap"> |
|
153 |
- <li v-for="(resultitem, index) in paginatedItems" :key="index" class="mb-30"> |
|
154 |
- <div class="text-box"> |
|
155 |
- <router-link :to="{ path: '/MediaVideoDetail.page' }"> |
|
156 |
- <h5>{{ resultitem.title }}</h5> |
|
157 |
- </router-link> |
|
158 |
- <p class="address">{{ resultitem.address }}</p> |
|
159 |
- |
|
160 |
- <div class="flex-sp-bw"> |
|
161 |
- <div class="mb-20"> |
|
162 |
- <ul class="category"> |
|
163 |
- <li v-if="resultitem.category1" class="category1">카테고리1</li> |
|
164 |
- <li v-if="resultitem.category2" class="category2">카테고리2</li> |
|
165 |
- </ul> |
|
166 |
- </div> |
|
167 |
- |
|
168 |
- <div class="date "> |
|
169 |
- <ul> |
|
170 |
- <li>생산연도 <b>{{ resultitem.year }}</b></li> |
|
171 |
- <li>|</li> |
|
172 |
- <li>등록 <b>{{ resultitem.date }}</b></li> |
|
173 |
- </ul> |
|
174 |
- </div> |
|
175 |
- </div> |
|
176 |
- </div> |
|
177 |
- </li> |
|
178 |
- </ul> |
|
179 |
- |
|
180 |
- <!-- Empty State if no results in paginatedItems --> |
|
181 |
- <div v-if="paginatedItems.length === 0" class="no-results"> |
|
182 |
- <p>등록된 게시물이 없습니다.</p> |
|
183 |
- </div> |
|
184 |
- </div> |
|
185 |
- </div> |
|
186 |
- </div> |
|
187 |
- </div> |
|
188 |
- </div> |
|
189 |
- |
|
190 |
- <div class="btn-group flex-end mt-40"><button class="register"> <router-link |
|
191 |
- :to="{ path: '/MediaVideoInsert.page' }">등록</router-link></button></div> |
|
192 |
- <div class="pagination flex-center mt-40"> |
|
193 |
- |
|
194 |
- <!-- Previous and Next Page Buttons --> |
|
195 |
- <button> |
|
196 |
- <DoubleLeftOutlined /> |
|
197 |
- </button> |
|
198 |
- <button @click="previousPage" :disabled="currentPage === 1"> |
|
199 |
- <LeftOutlined /> |
|
200 |
- </button> |
|
201 |
- <button class="page-number clicked">1</button> |
|
202 |
- <button @click="nextPage" :disabled="currentPage === totalPages"> |
|
203 |
- <RightOutlined /> |
|
204 |
- </button> |
|
205 |
- <button> |
|
206 |
- <DoubleRightOutlined /> |
|
207 |
- </button> |
|
208 |
- </div> |
|
209 |
- </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> |
|
210 | 14 |
</div> |
211 |
- |
|
15 |
+ <div class="search-form form"> |
|
16 |
+ <dl> |
|
17 |
+ <dd class="mb-15"> |
|
18 |
+ <p>검색범위</p> |
|
19 |
+ <ul> |
|
20 |
+ <li> |
|
21 |
+ <input type="checkbox" id="allScope" v-model="isChkAllScope" @change="fnChkAllOptions" /> |
|
22 |
+ <label for="allScope">전체</label> |
|
23 |
+ </li> |
|
24 |
+ <li> |
|
25 |
+ <input type="checkbox" id="searchSj" v-model="searchReqDTO.useSj" @change="fnChkOption" /> |
|
26 |
+ <label for="searchSj">제목</label> |
|
27 |
+ </li> |
|
28 |
+ <li> |
|
29 |
+ <input type="checkbox" id="searchCn" v-model="searchReqDTO.useCn" @change="fnChkOption" /> |
|
30 |
+ <label for="searchCn">내용</label> |
|
31 |
+ </li> |
|
32 |
+ </ul> |
|
33 |
+ </dd> |
|
34 |
+ <dd class="mb-15"> |
|
35 |
+ <p>검색어</p> |
|
36 |
+ <div class="wfull"><input type="text" v-model="searchReqDTO.searchText" v-on:keyup.enter="fnSearch()"></div> |
|
37 |
+ </dd> |
|
38 |
+ <dd class="mb-15"> |
|
39 |
+ <p>생산연도</p> |
|
40 |
+ <input type="date" v-model="searchReqDTO.startYear"> |
|
41 |
+ <p class="mark">~</p> |
|
42 |
+ <input type="date" v-model="searchReqDTO.endYear"> |
|
43 |
+ </dd> |
|
44 |
+ <dd class="mb-20 category-dd"> |
|
45 |
+ <p>카테고리</p> |
|
46 |
+ <ul> |
|
47 |
+ <li v-for="(category, idx) of categorys" :key="idx"> |
|
48 |
+ <input type="checkbox" :id="'ctgry_' + idx" name="categorys" :value="category.ctgryId" v-model="searchReqDTO.searchCtgries" /> |
|
49 |
+ <label :for="'ctgry_' + idx">{{ category.ctgryNm }}</label> |
|
50 |
+ </li> |
|
51 |
+ </ul> |
|
52 |
+ </dd> |
|
53 |
+ <dd class="mb-15"> |
|
54 |
+ <p>정렬</p> |
|
55 |
+ <ul> |
|
56 |
+ <li v-for="(order, idx) of orders" :key="idx"> |
|
57 |
+ <input type="radio" :id="order.key" name="orders" :value="order.key" v-model="searchReqDTO.order" /> |
|
58 |
+ <label :for="order.key">{{ order.value }}</label> |
|
59 |
+ </li> |
|
60 |
+ </ul> |
|
61 |
+ </dd> |
|
62 |
+ <div class="btn-group"> |
|
63 |
+ <button type="button" class="reset" @click="init"> |
|
64 |
+ <img :src="reseticon" alt=""> |
|
65 |
+ <p>초기화</p> |
|
66 |
+ </button> |
|
67 |
+ <button type="button" class="search" @click="fnSearch"> |
|
68 |
+ <img :src="searchicon" alt=""> |
|
69 |
+ <p>검색</p> |
|
70 |
+ </button> |
|
71 |
+ </div> |
|
72 |
+ </dl> |
|
73 |
+ </div> |
|
74 |
+ <div class="search-result"> |
|
75 |
+ <div class="tabs"> |
|
76 |
+ <div class="flex-sp-bw mb-20 align-center"> |
|
77 |
+ <div class="resultext "> |
|
78 |
+ <img :src="resulticon" alt=""> |
|
79 |
+ <p>총 <b>{{ searchReqDTO.totalRecordCount }}개</b>의 미디어 영상이 검색되었습니다. </p> |
|
80 |
+ </div> |
|
81 |
+ <div class="flex"> |
|
82 |
+ <ul class="tab-box mb-20"> |
|
83 |
+ <li v-for="(tab, idx) in tabs" :key="idx" class="tab-title" :class="{ active: selectedTabId === tab.id }" @click="selectTab(tab.id)"> |
|
84 |
+ <img :src="selectedTabId === tab.id ? tab.activeImage : tab.inactiveImage" :alt="tab.title" class="tab-icon" /> |
|
85 |
+ <p><b>{{ tab.title }}</b></p> |
|
86 |
+ </li> |
|
87 |
+ </ul> |
|
88 |
+ <div class="select-box"> |
|
89 |
+ <select v-model="searchReqDTO.recordSize" @change="fnSearch"> |
|
90 |
+ <option :value="24">24개</option> |
|
91 |
+ <option :value="36">36개</option> |
|
92 |
+ <option :value="100">100개</option> |
|
93 |
+ </select> |
|
94 |
+ </div> |
|
95 |
+ </div> |
|
96 |
+ </div> |
|
97 |
+ <div class="tab-content"> |
|
98 |
+ <div v-if="searchResult.length > 0"> |
|
99 |
+ <CardStyleComponent v-if="selectedTabId === 'CARD'" :name="'M'" :list="searchResult" /> |
|
100 |
+ <ListStyleComponent v-if="selectedTabId === 'LIST'" :name="'M'" :list="searchResult" /> |
|
101 |
+ </div> |
|
102 |
+ <div v-else class="no-results"> |
|
103 |
+ <p>등록된 게시물이 없습니다.</p> |
|
104 |
+ </div> |
|
105 |
+ </div> |
|
106 |
+ </div> |
|
107 |
+ <div class="btn-group flex-end mt-40"><button class="register"> <router-link :to="{ path: '/MediaVideoInsert.page' }">등록</router-link></button></div> |
|
108 |
+ <DefaultPagination class="mt-40" :search="searchReqDTO" @onChange="fnChangeCurrentPage" /> |
|
109 |
+ </div> |
|
110 |
+ </div> |
|
212 | 111 |
</template> |
213 | 112 |
<script> |
214 |
-import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'; |
|
215 |
-import { findAllCategoryProc } from "../../../resources/api/category"; // 카테고리 목록 검색 |
|
113 |
+// COMPONENT |
|
114 |
+import CardStyleComponent from '@/views/component/listLayout/CardStyleComponent.vue'; |
|
115 |
+import ListStyleComponent from '@/views/component/listLayout/ListStyleComponent.vue'; |
|
116 |
+import DefaultPagination from '@/views/component/DefaultPagination.vue'; |
|
117 |
+// API |
|
118 |
+import { findAllByNullProc } from "@/resources/api/category"; |
|
119 |
+import { findAllMediaVidosProc } from "@/resources/api/mediaVido"; |
|
216 | 120 |
|
217 | 121 |
export default { |
218 |
- components: { |
|
219 |
- DoubleLeftOutlined, |
|
220 |
- LeftOutlined, |
|
221 |
- RightOutlined, |
|
222 |
- DoubleRightOutlined, |
|
223 |
- }, |
|
224 |
- data() { |
|
225 |
- return { |
|
226 |
- selectedTab: 1, |
|
227 |
- // 검색용 객체 |
|
228 |
- searchReqDTO: { |
|
229 |
- searchType: [], |
|
230 |
- searchText: null, |
|
231 |
- startYear: null, |
|
232 |
- endYear: null, |
|
233 |
- searchTy: null, |
|
234 |
- searchCtgry: [], |
|
235 |
- order: "rgsde", |
|
236 |
- }, |
|
237 |
- tabs: [ |
|
122 |
+ components: { |
|
123 |
+ DefaultPagination, |
|
124 |
+ CardStyleComponent, |
|
125 |
+ ListStyleComponent, |
|
126 |
+ }, |
|
238 | 127 |
|
239 |
- { |
|
240 |
- id: 1, |
|
241 |
- title: "카드형", |
|
242 |
- activeImage: "client/resources/images/list_icon01_on.png", // Active tab image |
|
243 |
- inactiveImage: "client/resources/images/list_icon01_off.png", |
|
244 |
- }, |
|
245 |
- { |
|
246 |
- id: 2, |
|
247 |
- title: "리스트형", |
|
248 |
- activeImage: "client/resources/images/list_icon02_on.png", // Active tab image |
|
249 |
- inactiveImage: "client/resources/images/list_icon02_off.png", |
|
250 |
- }, |
|
251 |
- ], |
|
252 |
- tabContents: [ |
|
253 |
- { id: 1, viewType: 'card', list: [{ sj: 'Item 1', rgsde: '2025-03-01', files: [{ filePath: 'image1.png' }] }] }, |
|
254 |
- { id: 2, viewType: 'list', list: [{ sj: 'Item 2', rgsde: '2025-03-02', files: [{ filePath: 'image2.png' }] }] }, |
|
255 |
- ], |
|
256 |
- paginatedItems: [], |
|
257 |
- resultitems: [ |
|
258 |
- { |
|
259 |
- img: 'client/resources/images/img6.png', |
|
260 |
- title: '미디어 영상 제목', |
|
261 |
- address: '경상북도 구미시 송정대로 55', |
|
262 |
- content: '대한민국 최대의 내륙 산업단지를 보유하고, 서울로부터 277km, 부산으로부터 167km 거리에 있으며, 면적은 615㎢로 경상북도 전체 면적의 3.2%에 달합니다. 인구는 41만 명이고, 선산읍, 고아읍, 산동읍을 비롯한 3읍, 5면, 17개 동으로 구성되어…', |
|
263 |
- category1: true, |
|
264 |
- category2: true, |
|
265 |
- year: 2020, |
|
266 |
- date: '2021-01-01' |
|
267 |
- }, |
|
128 |
+ data() { |
|
129 |
+ return { |
|
130 |
+ // ICON |
|
131 |
+ resulticon: "client/resources/images/icon/r-check.png", |
|
132 |
+ homeicon: 'client/resources/images/icon/home.png', |
|
133 |
+ searchicon: 'client/resources/images/icon/search.png', |
|
134 |
+ reseticon: 'client/resources/images/icon/reset.png', |
|
135 |
+ righticon: 'client/resources/images/icon/right.png', |
|
268 | 136 |
|
269 |
- ], |
|
270 |
- currentPage: 1, // Current page number |
|
271 |
- itemsPerPage: 5, |
|
272 |
- resulticon: "client/resources/images/icon/r-check.png", |
|
273 |
- homeicon: 'client/resources/images/icon/home.png', |
|
274 |
- searchicon: 'client/resources/images/icon/search.png', |
|
275 |
- reseticon: 'client/resources/images/icon/reset.png', |
|
276 |
- righticon: 'client/resources/images/icon/right.png', |
|
277 |
- count: 23, |
|
278 |
- checkOptions: [ |
|
279 |
- '전체', |
|
280 |
- '사진', |
|
281 |
- '영상', |
|
282 |
- '미디어 영상', |
|
283 |
- '보도자료', |
|
284 |
- ], |
|
285 |
- checkOptions2: [ |
|
286 |
- '전체', |
|
287 |
- '제목', |
|
288 |
- '내용', |
|
289 |
- '주소', |
|
290 |
- ], |
|
291 |
- checkOptions3: [ |
|
292 |
- '카테고리1', |
|
293 |
- '카테고리2', |
|
294 |
- '카테고리3', |
|
295 |
- '카테고리4', |
|
296 |
- '카테고리5', |
|
297 |
- ], |
|
298 |
- checkOptions4: [ |
|
299 |
- '최신', |
|
300 |
- '인기', |
|
301 |
- ], |
|
302 |
- isChkAllScope: false, // 검색범위 전체 체크 여부 |
|
303 |
- searchType: [ |
|
304 |
- { key: "sj", value: "제목" }, |
|
305 |
- { key: "cn", value: "내용" }, |
|
306 |
- { key: "adres", value: "주소" }, |
|
307 |
- ], // 검색범위 목록 |
|
308 |
- categorys: [], // 카테고리 목록 |
|
309 |
- orders: [ |
|
310 |
- { key: "rgsde", value: "최신" }, |
|
311 |
- { key: "rdcnt", value: "인기" }, |
|
312 |
- ], // 정렬 목록 |
|
313 |
- }; |
|
314 |
- }, |
|
315 |
- computed: { |
|
316 |
- // Total number of pages |
|
317 |
- totalPages() { |
|
318 |
- return Math.ceil(this.resultitems.length / this.itemsPerPage); |
|
319 |
- }, |
|
137 |
+ // 검색용 객체 |
|
138 |
+ isChkAllScope: true, // 검색범위 전체 체크 여부 |
|
139 |
+ searchType: [ |
|
140 |
+ { key: "sj", value: "제목" }, |
|
141 |
+ { key: "cn", value: "내용" }, |
|
142 |
+ { key: "adres", value: "주소" }, |
|
143 |
+ ], // 검색범위 목록 |
|
144 |
+ categorys: [], // 카테고리 목록 |
|
145 |
+ orders: [ |
|
146 |
+ { key: "rgsde", value: "최신" }, |
|
147 |
+ { key: "rdcnt", value: "인기" }, |
|
148 |
+ ], // 정렬 목록 |
|
320 | 149 |
|
321 |
- // Paginated items based on current page and items per page |
|
322 |
- paginatedItems() { |
|
323 |
- const start = (this.currentPage - 1) * this.itemsPerPage; |
|
324 |
- const end = start + this.itemsPerPage; |
|
325 |
- return this.resultitems.slice(start, end); |
|
326 |
- }, |
|
327 |
- }, |
|
328 |
- created() { |
|
329 |
- // 초기 데이터 세팅 |
|
330 |
- this.isChkAllScope = true; |
|
331 |
- this.searchReqDTO.searchType = this.searchType.map(item => item.key); |
|
332 |
- this.searchReqDTO.order = this.orders[0].key |
|
150 |
+ // 검색용 객체 초기값 |
|
151 |
+ searchDefault: { |
|
152 |
+ useSj: true, |
|
153 |
+ useCn: true, |
|
154 |
+ searchText: null, |
|
155 |
+ startYear: null, |
|
156 |
+ endYear: null, |
|
157 |
+ searchCtgries: [], |
|
158 |
+ order: "rgsde", |
|
159 |
+ // 페이지네이션 |
|
160 |
+ currentPage: 1, // 현재 페이지 |
|
161 |
+ recordSize: 24, // 한 페이지에 표시할 데이터 개수 |
|
162 |
+ }, |
|
163 |
+ searchReqDTO: {}, // 실제 검색에 사용되는 객체 |
|
333 | 164 |
|
334 |
- this.fnFindCategorys(); // 카테고리 목록 조회 (검색조건 없음) |
|
335 |
- }, |
|
336 |
- methods: { |
|
337 |
- selectTab(tabId) { |
|
338 |
- this.selectedTab = tabId; // Update the selected tab index |
|
339 |
- }, |
|
340 |
- // Change the number of items displayed per page |
|
341 |
- changeItemsPerPage() { |
|
342 |
- this.currentPage = 1; // Reset to first page when changing items per page |
|
343 |
- }, |
|
344 |
- previousPage() { |
|
345 |
- if (this.currentPage > 1) { |
|
346 |
- this.currentPage--; |
|
347 |
- } |
|
348 |
- }, |
|
165 |
+ searchResult: [], // 검색결과 |
|
349 | 166 |
|
350 |
- // Go to the next page |
|
351 |
- nextPage() { |
|
352 |
- if (this.currentPage < this.totalPages) { |
|
353 |
- this.currentPage++; |
|
354 |
- } |
|
167 |
+ // 목록 레이아웃 |
|
168 |
+ selectedTabId: null, |
|
169 |
+ tabs: [ |
|
170 |
+ { |
|
171 |
+ id: "CARD", |
|
172 |
+ title: "카드형", |
|
173 |
+ activeImage: "client/resources/images/list_icon01_on.png", // Active tab image |
|
174 |
+ inactiveImage: "client/resources/images/list_icon01_off.png", |
|
355 | 175 |
}, |
356 |
- isChkAllScope: false, // 검색범위 전체 체크 여부 |
|
357 |
- searchType: [ |
|
358 |
- { key: "sj", value: "제목" }, |
|
359 |
- { key: "cn", value: "내용" }, |
|
360 |
- { key: "adres", value: "주소" }, |
|
361 |
- ], // 검색범위 목록 |
|
362 |
- categorys: [], // 카테고리 목록 |
|
363 |
- orders: [ |
|
364 |
- { key: "rgsde", value: "최신" }, |
|
365 |
- { key: "rdcnt", value: "인기" }, |
|
366 |
- ], // 정렬 목록 |
|
367 |
- |
|
368 |
- async fnFindCategorys() { |
|
369 |
- try { |
|
370 |
- const response = await findAllCategoryProc(); |
|
371 |
- this.categorys = response.data.data.ctgry; |
|
372 |
- } catch (error) { |
|
373 |
- if (error.response) { |
|
374 |
- console.log("에러 응답:", error.response.data); |
|
375 |
- } |
|
376 |
- console.error("Error:", error); |
|
377 |
- } |
|
176 |
+ { |
|
177 |
+ id: "LIST", |
|
178 |
+ title: "리스트형", |
|
179 |
+ activeImage: "client/resources/images/list_icon02_on.png", // Active tab image |
|
180 |
+ inactiveImage: "client/resources/images/list_icon02_off.png", |
|
378 | 181 |
}, |
182 |
+ ], |
|
379 | 183 |
|
380 |
- // 통합검색 |
|
381 |
- async fnFindAllDatas() { |
|
382 |
- try { |
|
383 |
- let params = {}; |
|
384 |
- if (this.searchReqDTO.searchRecord.length > 0) { |
|
385 |
- params.searchRecords = this.searchReqDTO.searchRecord.join(','); |
|
386 |
- } |
|
387 |
- if (this.searchReqDTO.searchType.length > 0) { |
|
388 |
- params.searchTypes = this.searchReqDTO.searchType.join(','); |
|
389 |
- } |
|
390 |
- if (this.searchReqDTO.searchCtgry.length > 0) { |
|
391 |
- params.searchCtgries = this.searchReqDTO.searchCtgry.join(','); |
|
392 |
- } |
|
393 |
- params.searchText = this.searchReqDTO.searchText; |
|
394 |
- params.startYear = this.searchReqDTO.startYear; |
|
395 |
- params.endYear = this.searchReqDTO.endYear; |
|
396 |
- params.order = this.searchReqDTO.order; |
|
184 |
+ isInitialLoad: true // 초기 로드 여부 |
|
185 |
+ }; |
|
186 |
+ }, |
|
397 | 187 |
|
398 |
- // API 호출 |
|
399 |
- const response = await findAllDatas(params); |
|
400 |
- this.searchResult = response.data.data.searchResult; |
|
401 |
- } catch (error) { |
|
402 |
- if (error.response) { |
|
403 |
- console.log("에러 응답:", error.response.data); |
|
404 |
- } |
|
405 |
- console.error("Error:", error); |
|
406 |
- } |
|
407 |
- }, |
|
188 |
+ created() { |
|
189 |
+ this.init(); // 초기화 |
|
190 |
+ this.fnFindCategorys(); // 카테고리 목록 조회 (검색조건 없음) |
|
191 |
+ }, |
|
192 |
+ |
|
193 |
+ mounted() { |
|
194 |
+ let searchText = this.$route.query.searchText; |
|
195 |
+ if (searchText !== null) { |
|
196 |
+ this.searchReqDTO.searchText = searchText; |
|
197 |
+ } |
|
198 |
+ |
|
199 |
+ this.fnSearch(); // 통합검색 |
|
200 |
+ }, |
|
201 |
+ |
|
202 |
+ methods: { |
|
203 |
+ // 초기화 |
|
204 |
+ init() { |
|
205 |
+ if (this.isInitialLoad) { |
|
206 |
+ this.isInitialLoad = false; |
|
207 |
+ } else { |
|
208 |
+ if (!confirm('검색 조건을 초기화하시겠습니까?')) { |
|
209 |
+ return; |
|
210 |
+ } |
|
211 |
+ } |
|
212 |
+ |
|
213 |
+ this.searchReqDTO = JSON.parse(JSON.stringify(this.searchDefault)); |
|
214 |
+ this.searchResult = []; // 검색결과 초기화 |
|
215 |
+ |
|
216 |
+ this.selectedTabId = this.tabs[0].id; |
|
217 |
+ |
|
218 |
+ this.fnSearch(); // 통합검색 |
|
408 | 219 |
}, |
409 | 220 |
|
221 |
+ // 카테고리 목록 조회 |
|
222 |
+ async fnFindCategorys() { |
|
223 |
+ try { |
|
224 |
+ const response = await findAllByNullProc(); |
|
225 |
+ this.categorys = response.data.data.ctgry; |
|
226 |
+ } catch (error) { |
|
227 |
+ this.categorys = []; // 카테고리 목록 초기화 |
|
410 | 228 |
|
229 |
+ if (error.response) { |
|
230 |
+ alert(error.response.data.message); |
|
231 |
+ } |
|
232 |
+ console.error(error.message); |
|
233 |
+ } |
|
234 |
+ }, |
|
411 | 235 |
|
236 |
+ // 페이지 이동 |
|
237 |
+ fnChangeCurrentPage(currentPage) { |
|
238 |
+ this.searchReqDTO.currentPage = Number(currentPage); |
|
239 |
+ this.fnFindCategorys(); |
|
240 |
+ }, |
|
241 |
+ |
|
242 |
+ // 통합검색 |
|
243 |
+ async fnSearch() { |
|
244 |
+ try { |
|
245 |
+ const params = JSON.parse(JSON.stringify(this.searchReqDTO)); |
|
246 |
+ |
|
247 |
+ // 카테고리 목록 처리 |
|
248 |
+ if (this.searchReqDTO.searchCtgries && this.searchReqDTO.searchCtgries.length > 0) { |
|
249 |
+ params.searchCtgries = this.searchReqDTO.searchCtgries.join(','); |
|
250 |
+ } else { |
|
251 |
+ delete params.searchCtgries; |
|
252 |
+ } |
|
253 |
+ |
|
254 |
+ // API 호출 |
|
255 |
+ const response = await findAllMediaVidosProc(params); |
|
256 |
+ this.searchResult = response.data.data.mediaVidos; |
|
257 |
+ this.searchReqDTO = response.data.data.search; |
|
258 |
+ } catch (error) { |
|
259 |
+ this.searchResult = []; // 검색결과 초기화 |
|
260 |
+ |
|
261 |
+ if (error.response) { |
|
262 |
+ alert(error.response.data.message); |
|
263 |
+ } |
|
264 |
+ console.error(error.message); |
|
265 |
+ } |
|
266 |
+ }, |
|
267 |
+ |
|
268 |
+ // 기록유형 전체 선택 여부 변경 |
|
269 |
+ fnChkAllOptions() { |
|
270 |
+ this.searchReqDTO.useSj = this.isChkAllScope; |
|
271 |
+ this.searchReqDTO.useCn = this.isChkAllScope; |
|
272 |
+ }, |
|
273 |
+ |
|
274 |
+ // 기록유형 선택 여부 변경 |
|
275 |
+ fnChkOption() { |
|
276 |
+ this.isChkAllScope = this.searchReqDTO.useSj && this.searchReqDTO.useCn; |
|
277 |
+ }, |
|
278 |
+ |
|
279 |
+ selectTab(tabId) { |
|
280 |
+ this.selectedTabId = tabId; |
|
281 |
+ }, |
|
282 |
+ }, |
|
412 | 283 |
}; |
413 |
-</script> |
|
414 |
-<style scoped></style>(파일 끝에 줄바꿈 문자 없음) |
|
284 |
+</script>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/user/PicHistoryDetail.vue
+++ client/views/pages/user/PicHistoryDetail.vue
... | ... | @@ -158,9 +158,16 @@ |
158 | 158 |
async fnFindDcry() { |
159 | 159 |
try { |
160 | 160 |
const response = await findDcryProc(this.pageId); |
161 |
+ |
|
162 |
+ if (response.data.data.dcry.ty !== 'P') { |
|
163 |
+ alert('올바른 접근이 아닙니다.'); |
|
164 |
+ this.fnMoveTo('PicHistorySearch'); // 목록으로 이동 |
|
165 |
+ } |
|
166 |
+ |
|
161 | 167 |
this.dcry = response.data.data.dcry; |
162 | 168 |
} catch (error) { |
163 | 169 |
alert('조회중 오류가 발생했습니다.'); |
170 |
+ this.fnMoveTo('PicHistorySearch'); |
|
164 | 171 |
|
165 | 172 |
if (error.response) { |
166 | 173 |
alert(error.response.data.message); |
--- client/views/pages/user/PicHistoryInsert.vue
+++ client/views/pages/user/PicHistoryInsert.vue
... | ... | @@ -160,6 +160,12 @@ |
160 | 160 |
async fnFindDcry() { |
161 | 161 |
try { |
162 | 162 |
const response = await findDcryProc(this.pageId); |
163 |
+ |
|
164 |
+ if (response.data.data.dcry.ty !== 'P') { |
|
165 |
+ alert('올바른 접근이 아닙니다.'); |
|
166 |
+ this.fnMoveTo('VideoHistorySearch'); // 목록으로 이동 |
|
167 |
+ } |
|
168 |
+ |
|
163 | 169 |
this.copyToDcryReqDTO(response.data.data.dcry); |
164 | 170 |
} catch (error) { |
165 | 171 |
alert('조회중 오류가 발생했습니다.'); |
--- client/views/pages/user/VideoHistoryDetail.vue
+++ client/views/pages/user/VideoHistoryDetail.vue
... | ... | @@ -15,10 +15,10 @@ |
15 | 15 |
<form action="" class="gallery-form mb-40"> |
16 | 16 |
<dl class="mb-20"> |
17 | 17 |
<dd> |
18 |
- <p>영상 기록물 제목1 </p> |
|
18 |
+ <p>{{ dcry.sj }}</p> |
|
19 | 19 |
<div class="date flex align-center"> |
20 | 20 |
<img :src="calendaricon" alt=""> |
21 |
- <span>2025.02.28</span> |
|
21 |
+ <span>{{ $dotFormatDate(dcry.rgsde) }}</span> |
|
22 | 22 |
</div> |
23 | 23 |
</dd> |
24 | 24 |
</dl> |
--- client/views/pages/user/VideoHistoryInsert.vue
+++ client/views/pages/user/VideoHistoryInsert.vue
... | ... | @@ -12,37 +12,36 @@ |
12 | 12 |
</ul> |
13 | 13 |
</div> |
14 | 14 |
</div> |
15 |
- <form action="" class="insert-form mb-50"> |
|
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> |
... | ... | @@ -51,26 +50,35 @@ |
51 | 50 |
<ul class="wfull"> |
52 | 51 |
<li class="flex align-center"> |
53 | 52 |
<p>파일첨부</p> |
54 |
- <div class="invalid-feedback"><img :src="erroricon" alt=""><span>첨부파일은 10건까지 등록 가능하며, 건당 최대 100MB를 초과할 수 없습니다.</span></div> |
|
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"> |
|
58 |
- <label for="fileInput" class="file-label mb-20"> |
|
59 |
- <div class="flex-center align-center"><img :src="fileicon" alt=""> |
|
56 |
+ <input type="file" id="fileInput" class="file-input" accept="video/mp4,video/quicktime,video/x-msvideo,video/x-ms-wmv,video/webm,video/x-matroska" @change="handleFileSelect"> |
|
57 |
+ <label for="fileInput" class="file-label mb-20" @dragover.prevent="handleDragOver" @dragleave.prevent="handleDragLeave" @drop.prevent="handleDrop" :class="{ 'drag-over': isDragging }"> |
|
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 |
- <!-- Corrected here: Use file.icon instead of fileicons.img --> |
|
70 |
- <img :src="file.icon" alt="fileicon"> |
|
70 |
+ <img src="/client/resources/images/icon/imgicon.png" alt="fileicon"> |
|
71 | 71 |
<p>{{ file.name }}</p> |
72 | 72 |
</div> |
73 |
- <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> |
|
74 | 82 |
</div> |
75 | 83 |
</div> |
76 | 84 |
</li> |
... | ... | @@ -79,17 +87,22 @@ |
79 | 87 |
</dl> |
80 | 88 |
</form> |
81 | 89 |
<div class="btn-group flex-center"> |
82 |
- <button class="cancel">취소</button> |
|
83 |
- <button class="register">등록</button> |
|
90 |
+ <button type="button" class="cancel" @click="fnMoveTo('VideoHistorySearch')">취소</button> |
|
91 |
+ <button type="button" class="register" @click="submitForm"> |
|
92 |
+ <span v-if="$isEmpty(pageId)">등록</span> |
|
93 |
+ <span v-else>수정</span> |
|
94 |
+ </button> |
|
84 | 95 |
</div> |
85 | 96 |
</div> |
86 |
- <CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" /> |
|
97 |
+ <CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" @addCtgries="fnAddCtgries" /> |
|
87 | 98 |
</template> |
88 | 99 |
<script> |
89 | 100 |
import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'; |
90 | 101 |
// COMPONENT |
91 | 102 |
import EditorComponent from '../../component/editor/EditorComponent.vue'; |
92 | 103 |
import CategorySelectModal from '../../component/modal/CategorySelectModal.vue'; |
104 |
+// API |
|
105 |
+import { findDcryProc, saveDcry, updateDcry } from '@/resources/api/dcry'; |
|
93 | 106 |
|
94 | 107 |
export default { |
95 | 108 |
components: { |
... | ... | @@ -97,116 +110,265 @@ |
97 | 110 |
LeftOutlined, |
98 | 111 |
RightOutlined, |
99 | 112 |
DoubleRightOutlined, |
100 |
- EditorComponent, CategorySelectModal, |
|
113 |
+ EditorComponent, |
|
114 |
+ CategorySelectModal, |
|
101 | 115 |
}, |
102 | 116 |
|
103 | 117 |
data() { |
104 | 118 |
return { |
105 |
- // Define the image sources |
|
119 |
+ // 아이콘 경로 |
|
106 | 120 |
homeicon: 'client/resources/images/icon/home.png', |
107 | 121 |
erroricon: 'client/resources/images/icon/error.png', |
108 | 122 |
righticon: 'client/resources/images/icon/right.png', |
109 | 123 |
fileicon: 'client/resources/images/icon/file.png', |
110 | 124 |
searchicon: 'client/resources/images/icon/search.png', |
111 | 125 |
|
112 |
- isModalOpen: false, |
|
126 |
+ pageId: null, |
|
113 | 127 |
|
114 |
- items: [ |
|
115 |
- { id: 1, category: '카테고리 1', selected: false }, |
|
116 |
- { id: 2, category: '카테고리 2', selected: false }, |
|
117 |
- { id: 3, category: '카테고리 3', selected: false }, |
|
118 |
- ], |
|
128 |
+ isModalOpen: false, |
|
129 |
+ isDragging: false, |
|
130 |
+ |
|
119 | 131 |
fileNames: [], |
120 |
- insertDTO: { |
|
121 |
- sj: null, //제목 |
|
122 |
- cn: null, //내용 |
|
123 |
- adres: null, // 주소 |
|
124 |
- prdctnYear: null, // 생산연도 |
|
125 |
- ty: 'P', // 타입 ( P: 사진, V: 영상 ) |
|
126 |
- multipartFiles: null, // 첨부파일 정보 |
|
127 |
- ctgryIds: null, // 카테고리 정보 |
|
132 |
+ |
|
133 |
+ // 등록/수정 요청 객체 |
|
134 |
+ reqDTO: { |
|
135 |
+ dcryId: null, |
|
136 |
+ sj: null, |
|
137 |
+ cn: null, |
|
138 |
+ adres: null, |
|
139 |
+ prdctnYear: null, |
|
140 |
+ ty: 'V', |
|
141 |
+ fileId: null, |
|
142 |
+ files: [], |
|
143 |
+ ctgryIds: [], |
|
128 | 144 |
}, |
129 | 145 |
|
130 |
- files: [], |
|
146 |
+ multipartFiles: [], |
|
131 | 147 |
selectedCtgries: [], // 카테고리 목록 |
132 | 148 |
}; |
133 | 149 |
}, |
134 |
- computed: { |
|
135 |
- filteredItems() { |
|
136 |
- // This could be modified to support filtering based on searchQuery |
|
137 |
- return this.items.filter(item => |
|
138 |
- item.category.includes(this.searchQuery) |
|
139 |
- ); |
|
150 |
+ |
|
151 |
+ created() { |
|
152 |
+ this.pageId = this.$route.query.id; |
|
153 |
+ if (!this.$isEmpty(this.pageId)) { |
|
154 |
+ this.fnFindDcry(); // 상세 조회 |
|
140 | 155 |
} |
141 | 156 |
}, |
142 |
- created() { |
|
143 |
- }, |
|
157 |
+ |
|
144 | 158 |
methods: { |
145 |
- registerCategories() { |
|
146 |
- // Add selected categories to the displayed list |
|
147 |
- this.selectedCtgries = this.items |
|
148 |
- .filter(item => item.selected) |
|
149 |
- .map(item => item.category); |
|
150 |
- this.closeModal(); // Close modal after registration |
|
151 |
- }, |
|
152 |
- removeCategory(index) { |
|
153 |
- // Remove category from the list |
|
154 |
- this.selectedCtgries.splice(index, 1); |
|
155 |
- }, |
|
156 |
- searchCategories() { |
|
157 |
- // You can implement search logic if needed |
|
158 |
- }, |
|
159 |
- nextPage() { |
|
160 |
- if (this.currentPage < this.totalPages) { |
|
161 |
- this.currentPage++; |
|
162 |
- } |
|
163 |
- }, |
|
164 |
- previousPage() { |
|
165 |
- if (this.currentPage > 1) { |
|
166 |
- this.currentPage--; |
|
167 |
- } |
|
168 |
- }, |
|
169 |
- showFileNames(event) { |
|
170 |
- const files = event.target.files; |
|
171 |
- this.fileNames = []; // Clear previous file names |
|
159 |
+ // 상세 조회 |
|
160 |
+ async fnFindDcry() { |
|
161 |
+ try { |
|
162 |
+ const response = await findDcryProc(this.pageId); |
|
172 | 163 |
|
173 |
- for (let i = 0; i < files.length; i++) { |
|
174 |
- const file = files[i]; |
|
175 |
- const fileType = file.name.split('.').pop().toLowerCase(); // Get file extension |
|
176 |
- |
|
177 |
- // Set default icon |
|
178 |
- let iconPath = this.fileicons; |
|
179 |
- |
|
180 |
- // Determine the icon based on file type |
|
181 |
- if (['jpg', 'jpeg', 'png', 'gif'].includes(fileType)) { |
|
182 |
- iconPath = 'client/resources/images/icon/imgicon.png'; // Example for image files |
|
183 |
- } else if (['pdf'].includes(fileType)) { |
|
184 |
- iconPath = 'client/resources/images/icon/pdficon.png'; // Example for PDF files |
|
185 |
- } else if (['xls'].includes(fileType)) { |
|
186 |
- iconPath = 'client/resources/images/icon/excelicon.png'; // Example for audio files |
|
187 |
- } else if (['hwp'].includes(fileType)) { |
|
188 |
- iconPath = 'client/resources/images/icon/hwpicon.png'; // Example for video files |
|
164 |
+ if (response.data.data.dcry.ty !== 'V') { |
|
165 |
+ alert('올바른 접근이 아닙니다.'); |
|
166 |
+ this.fnMoveTo('VideoHistorySearch'); // 목록으로 이동 |
|
189 | 167 |
} |
190 | 168 |
|
191 |
- // Push the file name and corresponding icon to the fileNames array |
|
192 |
- this.fileNames.push({ |
|
193 |
- name: file.name, |
|
194 |
- icon: iconPath |
|
195 |
- }); |
|
169 |
+ this.copyToDcryReqDTO(response.data.data.dcry); |
|
170 |
+ } catch (error) { |
|
171 |
+ alert('조회중 오류가 발생했습니다.'); |
|
172 |
+ this.fnMoveTo('VideoHistorySearch'); // 목록으로 이동 |
|
173 |
+ |
|
174 |
+ if (error.response) { |
|
175 |
+ alert(error.response.data.message); |
|
176 |
+ } |
|
177 |
+ console.error(error.message); |
|
196 | 178 |
} |
197 | 179 |
}, |
198 |
- removeFile(index) { |
|
199 |
- // Remove file from the list |
|
200 |
- this.fileNames.splice(index, 1); |
|
201 |
- console.log(removeFile) |
|
180 |
+ |
|
181 |
+ // dcry > reqDTO |
|
182 |
+ copyToDcryReqDTO(dcry) { |
|
183 |
+ const copyFields = Object.keys(this.reqDTO).filter(key => key !== 'dcryId' && key !== 'ty' && key !== 'files'); |
|
184 |
+ copyFields.forEach(field => { |
|
185 |
+ this.reqDTO[field] = this.$isEmpty(dcry[field]) ? null : dcry[field]; |
|
186 |
+ }); |
|
187 |
+ |
|
188 |
+ this.reqDTO.ty = 'V'; // 영상기록물 |
|
189 |
+ this.reqDTO.files = dcry.files.length > 0 ? dcry.files : []; // 기존 첨부파일 |
|
190 |
+ |
|
191 |
+ this.multipartFiles = []; |
|
192 |
+ this.selectedCtgries = dcry.ctgrys.length > 0 ? dcry.ctgrys : []; |
|
193 |
+ |
|
194 |
+ console.log(this.reqDTO); |
|
202 | 195 |
}, |
203 |
- openModal() { |
|
204 |
- this.isModalOpen = true; |
|
196 |
+ |
|
197 |
+ // 카테고리 모달 열기/닫기 |
|
198 |
+ fnToggleModal() { |
|
199 |
+ this.isModalOpen = !this.isModalOpen; |
|
205 | 200 |
}, |
206 |
- // 모달 닫기 |
|
207 |
- closeModal() { |
|
208 |
- this.isModalOpen = false; |
|
201 |
+ |
|
202 |
+ // 카테고리 등록 |
|
203 |
+ fnAddCtgries(selectedCtgries) { |
|
204 |
+ this.selectedCtgries = [...this.selectedCtgries, ...selectedCtgries]; |
|
205 |
+ |
|
206 |
+ this.fnToggleModal(); // 카테고리 모달 닫기 |
|
209 | 207 |
}, |
208 |
+ |
|
209 |
+ // 카테고리 삭제 |
|
210 |
+ fnDelCtgry(id) { |
|
211 |
+ this.selectedCtgries = this.selectedCtgries.filter(item => item.ctgryId !== id); |
|
212 |
+ }, |
|
213 |
+ |
|
214 |
+ // 드래그 앤 드롭 이벤트 핸들러 |
|
215 |
+ handleDragOver(event) { |
|
216 |
+ this.isDragging = true; |
|
217 |
+ }, |
|
218 |
+ handleDragLeave(event) { |
|
219 |
+ this.isDragging = false; |
|
220 |
+ }, |
|
221 |
+ handleDrop(event) { |
|
222 |
+ this.isDragging = false; |
|
223 |
+ |
|
224 |
+ const files = event.dataTransfer.files; |
|
225 |
+ if (files.length > 0) { |
|
226 |
+ this.processFiles(files); |
|
227 |
+ } |
|
228 |
+ }, |
|
229 |
+ handleFileSelect(event) { |
|
230 |
+ const files = event.target.files; |
|
231 |
+ if (files.length > 0) { |
|
232 |
+ this.processFiles(files); |
|
233 |
+ } |
|
234 |
+ }, |
|
235 |
+ |
|
236 |
+ // 파일 업로드 처리 함수 |
|
237 |
+ processFiles(files) { |
|
238 |
+ const allowedTypes = ['mp4', 'mov', 'avi', 'wmv', 'mkv', 'webm']; // 영상 파일만 허용 |
|
239 |
+ const maxSize = 10 * 1024 * 1024 * 1024; // 10GB |
|
240 |
+ |
|
241 |
+ // 유효성 검사 |
|
242 |
+ if (files.length > 1 + this.multipartFiles.length + this.reqDTO.files.length > 0) { |
|
243 |
+ alert("영상 파일은 한 개만 등록 가능합니다."); |
|
244 |
+ return; |
|
245 |
+ } |
|
246 |
+ |
|
247 |
+ for (let file of files) { |
|
248 |
+ const fileType = file.name.split('.').pop().toLowerCase(); |
|
249 |
+ |
|
250 |
+ // 파일 타입 검증 |
|
251 |
+ if (!allowedTypes.includes(fileType)) { |
|
252 |
+ alert(`${file.name} 파일은 허용되지 않는 형식입니다. 이미지 파일(jpg, jpeg, png, gif)만 업로드 가능합니다.`); |
|
253 |
+ return; |
|
254 |
+ } |
|
255 |
+ |
|
256 |
+ // 파일 크기 제한 검증 |
|
257 |
+ if (file.size > maxSize) { |
|
258 |
+ alert(`${file.name} 파일이 10GB를 초과합니다.`); |
|
259 |
+ return; |
|
260 |
+ } |
|
261 |
+ |
|
262 |
+ this.multipartFiles.push(file); |
|
263 |
+ } |
|
264 |
+ }, |
|
265 |
+ |
|
266 |
+ // 파일 삭제 |
|
267 |
+ fnDelFile(type, separator) { |
|
268 |
+ if (type === 'new') { |
|
269 |
+ this.multipartFiles.splice(separator, 1); |
|
270 |
+ } else if (type === 'old') { |
|
271 |
+ this.reqDTO.files = this.reqDTO.files.filter(item => item.fileId !== separator); |
|
272 |
+ } |
|
273 |
+ }, |
|
274 |
+ |
|
275 |
+ // 등록 |
|
276 |
+ async submitForm() { |
|
277 |
+ // 유효성 검사 |
|
278 |
+ if (!this.reqDTO.sj) { |
|
279 |
+ alert("제목을 입력해 주세요."); |
|
280 |
+ return; |
|
281 |
+ } |
|
282 |
+ if (this.$isEmpty(this.pageId) && this.multipartFiles.length == 0) { |
|
283 |
+ alert("파일을 1개 이상 첨부해 주세요."); |
|
284 |
+ return; |
|
285 |
+ } |
|
286 |
+ |
|
287 |
+ try { |
|
288 |
+ const formData = new FormData(); |
|
289 |
+ |
|
290 |
+ // 텍스트 데이터 추가 |
|
291 |
+ formData.append('sj', this.reqDTO.sj); |
|
292 |
+ formData.append('cn', this.reqDTO.cn); |
|
293 |
+ formData.append('adres', this.reqDTO.adres); |
|
294 |
+ formData.append('prdctnYear', this.reqDTO.prdctnYear); |
|
295 |
+ formData.append('ty', this.reqDTO.ty); |
|
296 |
+ |
|
297 |
+ // 게시물 아이디 |
|
298 |
+ if (!this.$isEmpty(this.pageId)) { |
|
299 |
+ formData.append('dcryId', this.pageId); |
|
300 |
+ } |
|
301 |
+ |
|
302 |
+ // 파일 아이디 |
|
303 |
+ if (!this.$isEmpty(this.reqDTO.fileId)) { |
|
304 |
+ formData.append('fileId', this.reqDTO.fileId); |
|
305 |
+ } |
|
306 |
+ |
|
307 |
+ // 카테고리 Ids 추가 |
|
308 |
+ if (this.selectedCtgries && this.selectedCtgries.length > 0) { |
|
309 |
+ for (let ctgry of this.selectedCtgries) { |
|
310 |
+ formData.append("ctgryIds", ctgry.ctgryId); |
|
311 |
+ } |
|
312 |
+ } |
|
313 |
+ |
|
314 |
+ // 파일 추가 |
|
315 |
+ for (let file of this.multipartFiles) { |
|
316 |
+ formData.append("multipartFiles", file); |
|
317 |
+ } |
|
318 |
+ |
|
319 |
+ // 기존파일 수정 |
|
320 |
+ if (!this.$isEmpty(this.pageId) && this.reqDTO.files.length > 0) { |
|
321 |
+ for (let file of this.reqDTO.files) { |
|
322 |
+ formData.append("files", file.fileId); |
|
323 |
+ } |
|
324 |
+ } |
|
325 |
+ |
|
326 |
+ // API 통신 |
|
327 |
+ const response = this.$isEmpty(this.pageId) ? await saveDcry(formData) : await updateDcry(formData); |
|
328 |
+ let result = response.data; |
|
329 |
+ let id = result.data.dcryId; |
|
330 |
+ alert(this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다."); |
|
331 |
+ |
|
332 |
+ // 상세 페이지로 이동 |
|
333 |
+ this.fnMoveTo('VideoHistoryDetail', id); |
|
334 |
+ } catch (error) { |
|
335 |
+ if (error.response) { |
|
336 |
+ alert(error.response.data.message); |
|
337 |
+ } else { |
|
338 |
+ alert("에러가 발생했습니다."); |
|
339 |
+ } |
|
340 |
+ console.error(error.message); |
|
341 |
+ }; |
|
342 |
+ }, |
|
343 |
+ |
|
344 |
+ // 페이지 이동 |
|
345 |
+ fnMoveTo(page, id) { |
|
346 |
+ if (this.$isEmpty(id)) { |
|
347 |
+ this.$router.push({ name: page }); |
|
348 |
+ } else { |
|
349 |
+ this.$router.push({ name: page, query: { id: id } }); |
|
350 |
+ } |
|
351 |
+ } |
|
210 | 352 |
} |
211 | 353 |
}; |
212 | 354 |
</script> |
355 |
+<style> |
|
356 |
+.file-label { |
|
357 |
+ border: 2px dashed #ddd; |
|
358 |
+ border-radius: 8px; |
|
359 |
+ padding: 20px; |
|
360 |
+ text-align: center; |
|
361 |
+ cursor: pointer; |
|
362 |
+ transition: all 0.3s ease; |
|
363 |
+} |
|
364 |
+ |
|
365 |
+.file-label:hover { |
|
366 |
+ border-color: #aaa; |
|
367 |
+ background-color: #f9f9f9; |
|
368 |
+} |
|
369 |
+ |
|
370 |
+.file-label.drag-over { |
|
371 |
+ border-color: #1890ff; |
|
372 |
+ background-color: rgba(24, 144, 255, 0.1); |
|
373 |
+} |
|
374 |
+</style>(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/user/VideoHistorySearch.vue
+++ client/views/pages/user/VideoHistorySearch.vue
... | ... | @@ -4,8 +4,8 @@ |
4 | 4 |
<h2>영상 기록물</h2> |
5 | 5 |
<div class="breadcrumb-list"> |
6 | 6 |
<ul> |
7 |
- <!-- Bind the image source dynamically for homeicon --> |
|
8 |
- <li><img :src="homeicon" alt="Home Icon"> |
|
7 |
+ <li> |
|
8 |
+ <img :src="homeicon" alt="Home Icon"> |
|
9 | 9 |
<p>기록물</p> |
10 | 10 |
</li> |
11 | 11 |
<li><img :src="righticon" alt=""></li> |
... | ... | @@ -13,24 +13,32 @@ |
13 | 13 |
</ul> |
14 | 14 |
</div> |
15 | 15 |
</div> |
16 |
- <div action="search" class="search-form form "> |
|
16 |
+ <div class="search-form form"> |
|
17 | 17 |
<dl> |
18 | 18 |
<dd class="mb-15"> |
19 | 19 |
<p>검색범위</p> |
20 | 20 |
<ul> |
21 | 21 |
<li> |
22 |
- <input type="checkbox" id="allScope" v-model="isChkAllScope" @change="fnChkAllOptions('scope')" /> |
|
22 |
+ <input type="checkbox" id="allScope" v-model="isChkAllScope" @change="fnChkAllOptions" /> |
|
23 | 23 |
<label for="allScope">전체</label> |
24 | 24 |
</li> |
25 |
- <li v-for="(scope, idx) in searchType" :key="idx"> |
|
26 |
- <input type="checkbox" :id="idx" :name="searchType" :value="scope.key" v-model="searchReqDTO.searchType" @change="fnChkOption('scope')" /> |
|
27 |
- <label :for="idx">{{ scope.value }}</label> |
|
25 |
+ <li> |
|
26 |
+ <input type="checkbox" id="searchSj" v-model="searchReqDTO.useSj" @change="fnChkOption" /> |
|
27 |
+ <label for="searchSj">제목</label> |
|
28 |
+ </li> |
|
29 |
+ <li> |
|
30 |
+ <input type="checkbox" id="searchCn" v-model="searchReqDTO.useCn" @change="fnChkOption" /> |
|
31 |
+ <label for="searchCn">내용</label> |
|
32 |
+ </li> |
|
33 |
+ <li> |
|
34 |
+ <input type="checkbox" id="searchAdres" v-model="searchReqDTO.useAdres" @change="fnChkOption" /> |
|
35 |
+ <label for="searchAdres">주소</label> |
|
28 | 36 |
</li> |
29 | 37 |
</ul> |
30 | 38 |
</dd> |
31 | 39 |
<dd class="mb-15"> |
32 | 40 |
<p>검색어</p> |
33 |
- <div class="wfull"><input type="text" v-model="searchReqDTO.searchText"></div> |
|
41 |
+ <div class="wfull"><input type="text" v-model="searchReqDTO.searchText" v-on:keyup.enter="fnSearch()"></div> |
|
34 | 42 |
</dd> |
35 | 43 |
<dd class="mb-15"> |
36 | 44 |
<p>생산연도</p> |
... | ... | @@ -42,8 +50,8 @@ |
42 | 50 |
<p>카테고리</p> |
43 | 51 |
<ul> |
44 | 52 |
<li v-for="(category, idx) of categorys" :key="idx"> |
45 |
- <input type="checkbox" :id="category.ctgryId" name="categorys" :value="category.ctgryId" v-model="searchReqDTO.searchCtgry" /> |
|
46 |
- <label :for="category.ctgryId">{{ category.ctgryNm }}</label> |
|
53 |
+ <input type="checkbox" :id="'ctgry_' + idx" name="categorys" :value="category.ctgryId" v-model="searchReqDTO.searchCtgries" /> |
|
54 |
+ <label :for="'ctgry_' + idx">{{ category.ctgryNm }}</label> |
|
47 | 55 |
</li> |
48 | 56 |
</ul> |
49 | 57 |
</dd> |
... | ... | @@ -57,10 +65,12 @@ |
57 | 65 |
</ul> |
58 | 66 |
</dd> |
59 | 67 |
<div class="btn-group"> |
60 |
- <button class="reset"><img :src="reseticon" alt=""> |
|
68 |
+ <button type="button" class="reset" @click="init"> |
|
69 |
+ <img :src="reseticon" alt=""> |
|
61 | 70 |
<p>초기화</p> |
62 | 71 |
</button> |
63 |
- <button class="search"><img :src="searchicon" alt=""> |
|
72 |
+ <button type="button" class="search" @click="fnSearch"> |
|
73 |
+ <img :src="searchicon" alt=""> |
|
64 | 74 |
<p>검색</p> |
65 | 75 |
</button> |
66 | 76 |
</div> |
... | ... | @@ -71,213 +81,66 @@ |
71 | 81 |
<div class="flex-sp-bw mb-20 align-center"> |
72 | 82 |
<div class="resultext "> |
73 | 83 |
<img :src="resulticon" alt=""> |
74 |
- <p>총 <b>{{ count }}개</b>의 영상 기록물이 검색되었습니다. </p> |
|
84 |
+ <p>총 <b>{{ searchReqDTO.totalRecordCount }}개</b>의 영상 기록물이 검색되었습니다. </p> |
|
75 | 85 |
</div> |
76 |
- <div class="flex "> |
|
86 |
+ <div class="flex"> |
|
77 | 87 |
<ul class="tab-box mb-20"> |
78 |
- <li v-for="(tab, index) in tabs" :key="index" class="tab-title" :class="{ active: selectedTab === tab.id }" @click="selectTab(tab.id)"> |
|
79 |
- <img :src="selectedTab === tab.id ? tab.activeImage : tab.inactiveImage" :alt="tab.title" class="tab-icon" /> |
|
88 |
+ <li v-for="(tab, idx) in tabs" :key="idx" class="tab-title" :class="{ active: selectedTabId === tab.id }" @click="selectTab(tab.id)"> |
|
89 |
+ <img :src="selectedTabId === tab.id ? tab.activeImage : tab.inactiveImage" :alt="tab.title" class="tab-icon" /> |
|
80 | 90 |
<p><b>{{ tab.title }}</b></p> |
81 | 91 |
</li> |
82 | 92 |
</ul> |
83 | 93 |
<div class="select-box"> |
84 |
- <select v-model="itemsPerPage" @change="changeItemsPerPage"> |
|
85 |
- <option :value="5" selected>5개</option> |
|
86 |
- <option :value="10">10개</option> |
|
87 |
- <option :value="15">15개</option> |
|
94 |
+ <select v-model="searchReqDTO.recordSize" @change="fnSearch"> |
|
95 |
+ <option :value="24">24개</option> |
|
96 |
+ <option :value="36">36개</option> |
|
97 |
+ <option :value="100">100개</option> |
|
88 | 98 |
</select> |
89 | 99 |
</div> |
90 | 100 |
</div> |
91 | 101 |
</div> |
92 | 102 |
<div class="tab-content"> |
93 |
- <!-- Loop through tabContents, and only display content that matches selectedTab --> |
|
94 |
- <div v-for="(tabContent, idx) in tabContents" :key="idx"> |
|
95 |
- <!-- Display content only if the tab's ID matches the selectedTab --> |
|
96 |
- <div v-show="tabContent.id === selectedTab"> |
|
97 |
- <!-- 카드형 Section (Card Layout) --> |
|
98 |
- <div v-if="tabContent.viewType === 'card'"> |
|
99 |
- <ul class="card-wrap"> |
|
100 |
- <li v-for="(resultitem, index) in paginatedItems" :key="index" class="mb-30"> |
|
101 |
- <div class="result-box"> |
|
102 |
- <!-- Main Image Section --> |
|
103 |
- <div class="main-img"> |
|
104 |
- <img :src="resultitem.img" alt="" class="tab-image" /> |
|
105 |
- </div> |
|
106 |
- <!-- Text Section --> |
|
107 |
- <div class="text-box"> |
|
108 |
- <router-link :to="{ path: '/VideoHistoryDetail.page' }"> |
|
109 |
- <h5>{{ resultitem.title }}</h5> |
|
110 |
- </router-link> |
|
111 |
- <p class="address">{{ resultitem.address }}</p> |
|
112 |
- <p class="text">{{ resultitem.content }}</p> |
|
113 |
- <div class="mb-20"> |
|
114 |
- <ul class="category"> |
|
115 |
- <li v-if="resultitem.category1" class="category1">카테고리1</li> |
|
116 |
- <li v-if="resultitem.category2" class="category2">카테고리2</li> |
|
117 |
- </ul> |
|
118 |
- </div> |
|
119 |
- <div class="date"> |
|
120 |
- <ul> |
|
121 |
- <li>생산연도 <b>{{ resultitem.year }}</b></li> |
|
122 |
- <li>|</li> |
|
123 |
- <li>등록 <b>{{ resultitem.date }}</b></li> |
|
124 |
- </ul> |
|
125 |
- </div> |
|
126 |
- </div> |
|
127 |
- </div> |
|
128 |
- </li> |
|
129 |
- </ul> |
|
130 |
- <!-- Empty State if no results in paginatedItems --> |
|
131 |
- <div v-if="paginatedItems.length === 0" class="no-results"> |
|
132 |
- <p>등록된 게시물이 없습니다.</p> |
|
133 |
- </div> |
|
134 |
- </div> |
|
135 |
- <!-- 리스트형 Section (List Layout) --> |
|
136 |
- <div v-if="tabContent.viewType === 'list'"> |
|
137 |
- <ul class="list-wrap"> |
|
138 |
- <li v-for="(resultitem, index) in paginatedItems" :key="index" class="mb-30"> |
|
139 |
- <div class="text-box"> |
|
140 |
- <router-link :to="{ path: '/VideoHistoryDetail.page' }"> |
|
141 |
- <h5>{{ resultitem.title }}</h5> |
|
142 |
- </router-link> |
|
143 |
- <p class="address">{{ resultitem.address }}</p> |
|
144 |
- <div class="flex-sp-bw"> |
|
145 |
- <div class="mb-20"> |
|
146 |
- <ul class="category"> |
|
147 |
- <li v-if="resultitem.category1" class="category1">카테고리1</li> |
|
148 |
- <li v-if="resultitem.category2" class="category2">카테고리2</li> |
|
149 |
- </ul> |
|
150 |
- </div> |
|
151 |
- <div class="date "> |
|
152 |
- <ul> |
|
153 |
- <li>생산연도 <b>{{ resultitem.year }}</b></li> |
|
154 |
- <li>|</li> |
|
155 |
- <li>등록 <b>{{ resultitem.date }}</b></li> |
|
156 |
- </ul> |
|
157 |
- </div> |
|
158 |
- </div> |
|
159 |
- </div> |
|
160 |
- </li> |
|
161 |
- </ul> |
|
162 |
- <!-- Empty State if no results in paginatedItems --> |
|
163 |
- <div v-if="paginatedItems.length === 0" class="no-results"> |
|
164 |
- <p>등록된 게시물이 없습니다.</p> |
|
165 |
- </div> |
|
166 |
- </div> |
|
167 |
- </div> |
|
103 |
+ <div v-if="searchResult.length > 0"> |
|
104 |
+ <CardStyleComponent v-if="selectedTabId === 'CARD'" :name="'V'" :list="searchResult" /> |
|
105 |
+ <ListStyleComponent v-if="selectedTabId === 'LIST'" :name="'V'" :list="searchResult" /> |
|
106 |
+ </div> |
|
107 |
+ <div v-else class="no-results"> |
|
108 |
+ <p>등록된 게시물이 없습니다.</p> |
|
168 | 109 |
</div> |
169 | 110 |
</div> |
170 | 111 |
</div> |
171 | 112 |
<div class="btn-group flex-end mt-40"><button class="register"> <router-link :to="{ path: '/VideoHistoryInsert.page' }">등록</router-link></button></div> |
172 |
- <div class="pagination flex-center mt-40"> |
|
173 |
- <!-- Previous and Next Page Buttons --> |
|
174 |
- <button> |
|
175 |
- <DoubleLeftOutlined /> |
|
176 |
- </button> |
|
177 |
- <button @click="previousPage" :disabled="currentPage === 1"> |
|
178 |
- <LeftOutlined /> |
|
179 |
- </button> |
|
180 |
- <button class="page-number clicked">1</button> |
|
181 |
- <button @click="nextPage" :disabled="currentPage === totalPages"> |
|
182 |
- <RightOutlined /> |
|
183 |
- </button> |
|
184 |
- <button> |
|
185 |
- <DoubleRightOutlined /> |
|
186 |
- </button> |
|
187 |
- </div> |
|
113 |
+ <DefaultPagination class="mt-40" :search="searchReqDTO" @onChange="fnChangeCurrentPage" /> |
|
188 | 114 |
</div> |
189 | 115 |
</div> |
190 | 116 |
</template> |
191 | 117 |
<script> |
192 |
-import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue'; |
|
193 |
-import { findAllCategoryProc } from "../../../resources/api/category"; // 카테고리 목록 검색 |
|
118 |
+// COMPONENT |
|
119 |
+import CardStyleComponent from '@/views/component/listLayout/CardStyleComponent.vue'; |
|
120 |
+import ListStyleComponent from '@/views/component/listLayout/ListStyleComponent.vue'; |
|
121 |
+import DefaultPagination from '@/views/component/DefaultPagination.vue'; |
|
122 |
+// API |
|
123 |
+import { findAllByNullProc } from "@/resources/api/category"; |
|
124 |
+import { findDcrysProc } from "@/resources/api/dcry"; |
|
194 | 125 |
|
195 | 126 |
export default { |
196 | 127 |
components: { |
197 |
- DoubleLeftOutlined, |
|
198 |
- LeftOutlined, |
|
199 |
- RightOutlined, |
|
200 |
- DoubleRightOutlined, |
|
128 |
+ DefaultPagination, |
|
129 |
+ CardStyleComponent, |
|
130 |
+ ListStyleComponent, |
|
201 | 131 |
}, |
132 |
+ |
|
202 | 133 |
data() { |
203 | 134 |
return { |
204 |
- selectedTab: 1, |
|
205 |
- // 검색용 객체 |
|
206 |
- searchReqDTO: { |
|
207 |
- searchType: [], |
|
208 |
- searchText: null, |
|
209 |
- startYear: null, |
|
210 |
- endYear: null, |
|
211 |
- searchTy: null, |
|
212 |
- searchCtgry: [], |
|
213 |
- order: "rgsde", |
|
214 |
- }, |
|
215 |
- tabs: [ |
|
216 |
- |
|
217 |
- { |
|
218 |
- id: 1, |
|
219 |
- title: "카드형", |
|
220 |
- activeImage: "client/resources/images/list_icon01_on.png", // Active tab image |
|
221 |
- inactiveImage: "client/resources/images/list_icon01_off.png", |
|
222 |
- }, |
|
223 |
- { |
|
224 |
- id: 2, |
|
225 |
- title: "리스트형", |
|
226 |
- activeImage: "client/resources/images/list_icon02_on.png", // Active tab image |
|
227 |
- inactiveImage: "client/resources/images/list_icon02_off.png", |
|
228 |
- }, |
|
229 |
- ], |
|
230 |
- tabContents: [ |
|
231 |
- { id: 1, viewType: 'card', list: [{ sj: 'Item 1', rgsde: '2025-03-01', files: [{ filePath: 'image1.png' }] }] }, |
|
232 |
- { id: 2, viewType: 'list', list: [{ sj: 'Item 2', rgsde: '2025-03-02', files: [{ filePath: 'image2.png' }] }] }, |
|
233 |
- ], |
|
234 |
- paginatedItems: [], |
|
235 |
- resultitems: [ |
|
236 |
- { |
|
237 |
- img: 'client/resources/images/img6.png', |
|
238 |
- title: '영상 기록물 제목', |
|
239 |
- address: '경상북도 구미시 송정대로 55', |
|
240 |
- content: '대한민국 최대의 내륙 산업단지를 보유하고, 서울로부터 277km, 부산으로부터 167km 거리에 있으며, 면적은 615㎢로 경상북도 전체 면적의 3.2%에 달합니다. 인구는 41만 명이고, 선산읍, 고아읍, 산동읍을 비롯한 3읍, 5면, 17개 동으로 구성되어…', |
|
241 |
- category1: true, |
|
242 |
- category2: true, |
|
243 |
- year: 2020, |
|
244 |
- date: '2021-01-01' |
|
245 |
- }, |
|
246 |
- |
|
247 |
- ], |
|
248 |
- currentPage: 1, // Current page number |
|
249 |
- itemsPerPage: 5, |
|
135 |
+ // ICON |
|
250 | 136 |
resulticon: "client/resources/images/icon/r-check.png", |
251 | 137 |
homeicon: 'client/resources/images/icon/home.png', |
252 | 138 |
searchicon: 'client/resources/images/icon/search.png', |
253 | 139 |
reseticon: 'client/resources/images/icon/reset.png', |
254 | 140 |
righticon: 'client/resources/images/icon/right.png', |
255 |
- count: 23, |
|
256 |
- checkOptions: [ |
|
257 |
- '전체', |
|
258 |
- '사진', |
|
259 |
- '영상', |
|
260 |
- '미디어 영상', |
|
261 |
- '보도자료', |
|
262 |
- ], |
|
263 |
- checkOptions2: [ |
|
264 |
- '전체', |
|
265 |
- '제목', |
|
266 |
- '내용', |
|
267 |
- '주소', |
|
268 |
- ], |
|
269 |
- checkOptions3: [ |
|
270 |
- '카테고리1', |
|
271 |
- '카테고리2', |
|
272 |
- '카테고리3', |
|
273 |
- '카테고리4', |
|
274 |
- '카테고리5', |
|
275 |
- ], |
|
276 |
- checkOptions4: [ |
|
277 |
- '최신', |
|
278 |
- '인기', |
|
279 |
- ], |
|
280 |
- isChkAllScope: false, // 검색범위 전체 체크 여부 |
|
141 |
+ |
|
142 |
+ // 검색용 객체 |
|
143 |
+ isChkAllScope: true, // 검색범위 전체 체크 여부 |
|
281 | 144 |
searchType: [ |
282 | 145 |
{ key: "sj", value: "제목" }, |
283 | 146 |
{ key: "cn", value: "내용" }, |
... | ... | @@ -288,105 +151,144 @@ |
288 | 151 |
{ key: "rgsde", value: "최신" }, |
289 | 152 |
{ key: "rdcnt", value: "인기" }, |
290 | 153 |
], // 정렬 목록 |
154 |
+ |
|
155 |
+ // 검색용 객체 초기값 |
|
156 |
+ searchDefault: { |
|
157 |
+ useSj: true, |
|
158 |
+ useCn: true, |
|
159 |
+ useAdres: true, |
|
160 |
+ searchText: null, |
|
161 |
+ startYear: null, |
|
162 |
+ endYear: null, |
|
163 |
+ searchTy: "V", |
|
164 |
+ searchCtgries: [], |
|
165 |
+ order: "rgsde", |
|
166 |
+ // 페이지네이션 |
|
167 |
+ currentPage: 1, // 현재 페이지 |
|
168 |
+ recordSize: 24, // 한 페이지에 표시할 데이터 개수 |
|
169 |
+ }, |
|
170 |
+ searchReqDTO: {}, // 실제 검색에 사용되는 객체 |
|
171 |
+ |
|
172 |
+ searchResult: [], // 검색결과 |
|
173 |
+ |
|
174 |
+ // 목록 레이아웃 |
|
175 |
+ selectedTabId: null, |
|
176 |
+ tabs: [ |
|
177 |
+ { |
|
178 |
+ id: "CARD", |
|
179 |
+ title: "카드형", |
|
180 |
+ activeImage: "client/resources/images/list_icon01_on.png", // Active tab image |
|
181 |
+ inactiveImage: "client/resources/images/list_icon01_off.png", |
|
182 |
+ }, |
|
183 |
+ { |
|
184 |
+ id: "LIST", |
|
185 |
+ title: "리스트형", |
|
186 |
+ activeImage: "client/resources/images/list_icon02_on.png", // Active tab image |
|
187 |
+ inactiveImage: "client/resources/images/list_icon02_off.png", |
|
188 |
+ }, |
|
189 |
+ ], |
|
190 |
+ |
|
191 |
+ isInitialLoad: true // 초기 로드 여부 |
|
291 | 192 |
}; |
292 | 193 |
}, |
293 |
- computed: { |
|
294 |
- // Total number of pages |
|
295 |
- totalPages() { |
|
296 |
- return Math.ceil(this.resultitems.length / this.itemsPerPage); |
|
297 |
- }, |
|
298 | 194 |
|
299 |
- // Paginated items based on current page and items per page |
|
300 |
- paginatedItems() { |
|
301 |
- const start = (this.currentPage - 1) * this.itemsPerPage; |
|
302 |
- const end = start + this.itemsPerPage; |
|
303 |
- return this.resultitems.slice(start, end); |
|
304 |
- }, |
|
305 |
- }, |
|
306 | 195 |
created() { |
307 |
- // 초기 데이터 세팅 |
|
308 |
- this.isChkAllScope = true; |
|
309 |
- this.searchReqDTO.searchType = this.searchType.map(item => item.key); |
|
310 |
- this.searchReqDTO.order = this.orders[0].key |
|
311 |
- |
|
196 |
+ this.init(); // 초기화 |
|
312 | 197 |
this.fnFindCategorys(); // 카테고리 목록 조회 (검색조건 없음) |
313 | 198 |
}, |
199 |
+ |
|
200 |
+ mounted() { |
|
201 |
+ let searchText = this.$route.query.searchText; |
|
202 |
+ if (searchText !== null) { |
|
203 |
+ this.searchReqDTO.searchText = searchText; |
|
204 |
+ } |
|
205 |
+ |
|
206 |
+ this.fnSearch(); // 통합검색 |
|
207 |
+ }, |
|
208 |
+ |
|
314 | 209 |
methods: { |
315 |
- selectTab(tabId) { |
|
316 |
- this.selectedTab = tabId; // Update the selected tab index |
|
317 |
- }, |
|
318 |
- // Change the number of items displayed per page |
|
319 |
- changeItemsPerPage() { |
|
320 |
- this.currentPage = 1; // Reset to first page when changing items per page |
|
321 |
- }, |
|
322 |
- previousPage() { |
|
323 |
- if (this.currentPage > 1) { |
|
324 |
- this.currentPage--; |
|
210 |
+ // 초기화 |
|
211 |
+ init() { |
|
212 |
+ if (this.isInitialLoad) { |
|
213 |
+ this.isInitialLoad = false; |
|
214 |
+ } else { |
|
215 |
+ if (!confirm('검색 조건을 초기화하시겠습니까?')) { |
|
216 |
+ return; |
|
217 |
+ } |
|
325 | 218 |
} |
219 |
+ |
|
220 |
+ this.searchReqDTO = JSON.parse(JSON.stringify(this.searchDefault)); |
|
221 |
+ this.searchResult = []; // 검색결과 초기화 |
|
222 |
+ |
|
223 |
+ this.selectedTabId = this.tabs[0].id; |
|
224 |
+ |
|
225 |
+ this.fnSearch(); // 통합검색 |
|
326 | 226 |
}, |
327 | 227 |
|
328 |
- // Go to the next page |
|
329 |
- nextPage() { |
|
330 |
- if (this.currentPage < this.totalPages) { |
|
331 |
- this.currentPage++; |
|
332 |
- } |
|
333 |
- }, |
|
334 |
- isChkAllScope: false, // 검색범위 전체 체크 여부 |
|
335 |
- searchType: [ |
|
336 |
- { key: "sj", value: "제목" }, |
|
337 |
- { key: "cn", value: "내용" }, |
|
338 |
- { key: "adres", value: "주소" }, |
|
339 |
- ], // 검색범위 목록 |
|
340 |
- categorys: [], // 카테고리 목록 |
|
341 |
- orders: [ |
|
342 |
- { key: "rgsde", value: "최신" }, |
|
343 |
- { key: "rdcnt", value: "인기" }, |
|
344 |
- ], // 정렬 목록 |
|
345 |
- |
|
228 |
+ // 카테고리 목록 조회 |
|
346 | 229 |
async fnFindCategorys() { |
347 | 230 |
try { |
348 |
- const response = await findAllCategoryProc(); |
|
231 |
+ const response = await findAllByNullProc(); |
|
349 | 232 |
this.categorys = response.data.data.ctgry; |
350 | 233 |
} catch (error) { |
234 |
+ this.categorys = []; // 카테고리 목록 초기화 |
|
235 |
+ |
|
351 | 236 |
if (error.response) { |
352 |
- console.log("에러 응답:", error.response.data); |
|
237 |
+ alert(error.response.data.message); |
|
353 | 238 |
} |
354 |
- console.error("Error:", error); |
|
239 |
+ console.error(error.message); |
|
355 | 240 |
} |
241 |
+ }, |
|
242 |
+ |
|
243 |
+ // 페이지 이동 |
|
244 |
+ fnChangeCurrentPage(currentPage) { |
|
245 |
+ this.searchReqDTO.currentPage = Number(currentPage); |
|
246 |
+ this.$nextTick(() => { |
|
247 |
+ this.fnSearch(); |
|
248 |
+ }); |
|
356 | 249 |
}, |
357 | 250 |
|
358 | 251 |
// 통합검색 |
359 |
- async fnFindAllDatas() { |
|
252 |
+ async fnSearch() { |
|
360 | 253 |
try { |
361 |
- let params = {}; |
|
362 |
- if (this.searchReqDTO.searchRecord.length > 0) { |
|
363 |
- params.searchRecords = this.searchReqDTO.searchRecord.join(','); |
|
254 |
+ const params = JSON.parse(JSON.stringify(this.searchReqDTO)); |
|
255 |
+ |
|
256 |
+ // 카테고리 목록 처리 |
|
257 |
+ if (this.searchReqDTO.searchCtgries && this.searchReqDTO.searchCtgries.length > 0) { |
|
258 |
+ params.searchCtgries = this.searchReqDTO.searchCtgries.join(','); |
|
259 |
+ } else { |
|
260 |
+ delete params.searchCtgries; |
|
364 | 261 |
} |
365 |
- if (this.searchReqDTO.searchType.length > 0) { |
|
366 |
- params.searchTypes = this.searchReqDTO.searchType.join(','); |
|
367 |
- } |
|
368 |
- if (this.searchReqDTO.searchCtgry.length > 0) { |
|
369 |
- params.searchCtgries = this.searchReqDTO.searchCtgry.join(','); |
|
370 |
- } |
|
371 |
- params.searchText = this.searchReqDTO.searchText; |
|
372 |
- params.startYear = this.searchReqDTO.startYear; |
|
373 |
- params.endYear = this.searchReqDTO.endYear; |
|
374 |
- params.order = this.searchReqDTO.order; |
|
375 | 262 |
|
376 | 263 |
// API 호출 |
377 |
- const response = await findAllDatas(params); |
|
378 |
- this.searchResult = response.data.data.searchResult; |
|
264 |
+ const response = await findDcrysProc(params); |
|
265 |
+ this.searchResult = response.data.data.dcrys; |
|
266 |
+ this.searchReqDTO = response.data.data.search; |
|
379 | 267 |
} catch (error) { |
268 |
+ this.searchResult = []; // 검색결과 초기화 |
|
269 |
+ |
|
380 | 270 |
if (error.response) { |
381 |
- console.log("에러 응답:", error.response.data); |
|
271 |
+ alert(error.response.data.message); |
|
382 | 272 |
} |
383 |
- console.error("Error:", error); |
|
273 |
+ console.error(error.message); |
|
384 | 274 |
} |
385 | 275 |
}, |
276 |
+ |
|
277 |
+ // 기록유형 전체 선택 여부 변경 |
|
278 |
+ fnChkAllOptions() { |
|
279 |
+ this.searchReqDTO.useSj = this.isChkAllScope; |
|
280 |
+ this.searchReqDTO.useCn = this.isChkAllScope; |
|
281 |
+ this.searchReqDTO.useAdres = this.isChkAllScope; |
|
282 |
+ }, |
|
283 |
+ |
|
284 |
+ // 기록유형 선택 여부 변경 |
|
285 |
+ fnChkOption() { |
|
286 |
+ this.isChkAllScope = this.searchReqDTO.useSj && this.searchReqDTO.useCn && this.searchReqDTO.useAdres; |
|
287 |
+ }, |
|
288 |
+ |
|
289 |
+ selectTab(tabId) { |
|
290 |
+ this.selectedTabId = tabId; |
|
291 |
+ }, |
|
386 | 292 |
}, |
387 |
- |
|
388 |
- |
|
389 |
- |
|
390 | 293 |
}; |
391 |
-</script> |
|
392 |
-<style scoped></style>(파일 끝에 줄바꿈 문자 없음) |
|
294 |
+</script>(파일 끝에 줄바꿈 문자 없음) |
--- package.json
+++ package.json
... | ... | @@ -18,7 +18,6 @@ |
18 | 18 |
"vue": "3.5.13", |
19 | 19 |
"vue-loader": "^17.2.2", |
20 | 20 |
"vue-router": "^4.3.2", |
21 |
- "vue3-youtube": "^0.1.9", |
|
22 | 21 |
"vuex": "^4.1.0", |
23 | 22 |
"vuex-persistedstate": "^4.1.0", |
24 | 23 |
"webpack": "5.74.0", |
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?