
+++ client/resources/api/category.js
... | ... | @@ -0,0 +1,6 @@ |
1 | +import apiClient from "./index"; | |
2 | + | |
3 | +// 카테고리 목록 조회 | |
4 | +export const findAllCategoryProc = mber => { | |
5 | + return apiClient.post(`/category/findAllCategory.json`, mber); | |
6 | +}(파일 끝에 줄바꿈 문자 없음) |
--- client/resources/api/index.js
+++ client/resources/api/index.js
... | ... | @@ -2,73 +2,148 @@ |
2 | 2 |
import store from "../../views/pages/AppStore"; |
3 | 3 |
|
4 | 4 |
const apiClient = axios.create({ |
5 |
- headers: {'Content-Type': 'application/json; charset=UTF-8'} |
|
6 |
-}) |
|
5 |
+ headers: { 'Content-Type': 'application/json; charset=UTF-8' } |
|
6 |
+}); |
|
7 |
+ |
|
8 |
+// 동시에 여러 요청이 401 에러가 발생했을 때 관리하기 위한 변수 |
|
9 |
+let isRefreshing = false; |
|
10 |
+let failedQueue = []; |
|
11 |
+ |
|
12 |
+const processQueue = (error, token = null) => { |
|
13 |
+ failedQueue.forEach(prom => { |
|
14 |
+ if (error) { |
|
15 |
+ prom.reject(error); |
|
16 |
+ } else { |
|
17 |
+ prom.resolve(token); |
|
18 |
+ } |
|
19 |
+ }); |
|
20 |
+ |
|
21 |
+ failedQueue = []; |
|
22 |
+}; |
|
7 | 23 |
|
8 | 24 |
apiClient.interceptors.request.use( |
9 |
- config => { |
|
10 |
- config.headers.Authorization = store.state.authorization // 요청 시 AccessToken 추가 |
|
11 |
- return config; |
|
12 |
- }, |
|
13 |
- error => { |
|
14 |
- return Promise.reject(error); |
|
25 |
+ config => { |
|
26 |
+ // Bearer 접두사 추가 (API 요구사항에 따라 필요한 경우) |
|
27 |
+ if (store.state.authorization) { |
|
28 |
+ config.headers.Authorization = store.state.authorization; |
|
15 | 29 |
} |
16 |
-) |
|
30 |
+ return config; |
|
31 |
+ }, |
|
32 |
+ error => { |
|
33 |
+ return Promise.reject(error); |
|
34 |
+ } |
|
35 |
+); |
|
17 | 36 |
|
18 | 37 |
async function refreshAccessToken() { |
19 |
- try { |
|
20 |
- const refreshToken = store.state.authorization; // 스토어에서 리프레시 토큰 가져오기 |
|
21 |
- |
|
22 |
- // 리프레시 토큰을 포함하여 재발급 요청 |
|
23 |
- const res = await axios.post('/refresh/tknReissue.json', {}, { |
|
24 |
- withCredentials: true // 쿠키를 요청에 포함시키기 위한 옵션 |
|
25 |
- }); |
|
26 |
- |
|
27 |
- if (res.headers.authorization) { |
|
28 |
- // 새로 발급받은 AccessToken 저장 |
|
29 |
- store.commit('setAuthorization', res.headers.authorization); |
|
30 |
- // 필요한 경우 리프레시 토큰도 업데이트 |
|
31 |
- // store.commit('setRefresh', res.headers.refresh); |
|
32 |
- } |
|
33 |
- |
|
34 |
- return res; // 응답 반환 |
|
35 |
- } catch (error) { |
|
36 |
- console.error('Error refreshing access token:', error); |
|
37 |
- alert('토큰 재발급에 실패했습니다. 다시 로그인 해주세요.'); |
|
38 |
- store.commit("setStoreReset"); |
|
39 |
- window.location = '/login.page'; // 로그인 페이지로 리다이렉트 |
|
38 |
+ try { |
|
39 |
+ // 리프레시 토큰 존재 여부 확인 |
|
40 |
+ if (!store.state.refreshToken) { |
|
41 |
+ throw new Error('리프레시 토큰이 없습니다.'); |
|
40 | 42 |
} |
43 |
+ |
|
44 |
+ console.log('토큰 재발급 시도:', new Date().toISOString()); |
|
45 |
+ |
|
46 |
+ // 서버의 구현에 맞게 요청 형식 수정 |
|
47 |
+ // 백엔드는 요청 바디가 아닌 쿠키에서 refreshToken을 읽으므로 |
|
48 |
+ // 빈 객체로 보내고 withCredentials를 true로 설정 |
|
49 |
+ const res = await axios.post('/refresh/tknReissue.json', {}, { |
|
50 |
+ withCredentials: true, |
|
51 |
+ timeout: 10000 // 10초 타임아웃 설정 |
|
52 |
+ }); |
|
53 |
+ |
|
54 |
+ console.log('토큰 재발급 응답:', res.status); |
|
55 |
+ |
|
56 |
+ // 백엔드는 응답 헤더에 Authorization으로 새 토큰을 보냄 |
|
57 |
+ const newToken = res.headers['authorization']; |
|
58 |
+ |
|
59 |
+ if (newToken) { |
|
60 |
+ // 새 토큰을 스토어에 저장 |
|
61 |
+ store.commit('setAuthorization', newToken); |
|
62 |
+ return newToken; |
|
63 |
+ } else { |
|
64 |
+ console.error('응답 헤더에 토큰이 없습니다:', res.headers); |
|
65 |
+ throw new Error('토큰이 포함되어 있지 않습니다.'); |
|
66 |
+ } |
|
67 |
+ } catch (error) { |
|
68 |
+ console.error('토큰 재발급 오류:', error); |
|
69 |
+ |
|
70 |
+ // 네트워크나 서버 오류 구분 |
|
71 |
+ if (!navigator.onLine) { |
|
72 |
+ alert('네트워크 연결을 확인해주세요.'); |
|
73 |
+ } else if (error.response && (error.response.status === 401 || error.response.status === 403)) { |
|
74 |
+ alert('세션이 만료되었습니다. 다시 로그인해 주세요.'); |
|
75 |
+ store.commit("setStoreReset"); |
|
76 |
+ window.location = '/login.page'; |
|
77 |
+ } else { |
|
78 |
+ alert('토큰 재발급에 실패했습니다. 다시 로그인해 주세요.'); |
|
79 |
+ store.commit("setStoreReset"); |
|
80 |
+ window.location = '/login.page'; |
|
81 |
+ } |
|
82 |
+ |
|
83 |
+ throw error; |
|
84 |
+ } |
|
41 | 85 |
} |
42 | 86 |
|
43 | 87 |
apiClient.interceptors.response.use( |
44 |
- response => { |
|
45 |
- return response; |
|
46 |
- }, |
|
47 |
- async error => { |
|
48 |
- const originalReq = error.config; |
|
88 |
+ response => { |
|
89 |
+ return response; |
|
90 |
+ }, |
|
91 |
+ async error => { |
|
92 |
+ const originalReq = error.config; |
|
49 | 93 |
|
50 |
- // 403 오류 처리: 접근 권한 없음 |
|
51 |
- if (error.response.status === 403 && error.response.data.message === '접근 권한이 없습니다.') { |
|
52 |
- window.history.back(); |
|
53 |
- return Promise.reject(error); |
|
54 |
- } |
|
55 |
- |
|
56 |
- // 401 오류 처리: 토큰 만료 |
|
57 |
- if (error.response.status === 401 && !originalReq._retry) { |
|
58 |
- originalReq._retry = true; // 재요청 시도 플래그 설정 |
|
59 |
- try { |
|
60 |
- // 액세스 토큰 재발급 요청 |
|
61 |
- await refreshAccessToken(); // 리프레시 함수 호출 |
|
62 |
- |
|
63 |
- // 원래 요청 재시도 |
|
64 |
- return apiClient(originalReq); |
|
65 |
- } catch (refreshError) { |
|
66 |
- return Promise.reject(refreshError); |
|
67 |
- } |
|
68 |
- } |
|
69 |
- |
|
70 |
- return Promise.reject(error); |
|
94 |
+ // 응답이 없는 경우(네트워크 오류) 처리 |
|
95 |
+ if (!error.response) { |
|
96 |
+ console.error('네트워크 오류:', error); |
|
97 |
+ return Promise.reject(error); |
|
71 | 98 |
} |
72 |
-) |
|
73 | 99 |
|
74 |
-export default apiClient; |
|
100 |
+ // 403 오류 처리: 접근 권한 없음 |
|
101 |
+ if (error.response.status === 403 && error.response.data.message === '접근 권한이 없습니다.') { |
|
102 |
+ window.history.back(); |
|
103 |
+ return Promise.reject(error); |
|
104 |
+ } |
|
105 |
+ |
|
106 |
+ // 401 오류 처리: 토큰 만료 |
|
107 |
+ if (error.response.status === 401 && !originalReq._retry) { |
|
108 |
+ originalReq._retry = true; |
|
109 |
+ |
|
110 |
+ // 이미 토큰 재발급 중인 경우 대기열에 추가 |
|
111 |
+ if (isRefreshing) { |
|
112 |
+ return new Promise((resolve, reject) => { |
|
113 |
+ failedQueue.push({ resolve, reject }); |
|
114 |
+ }).then(token => { |
|
115 |
+ originalReq.headers['Authorization'] = token; |
|
116 |
+ return apiClient(originalReq); |
|
117 |
+ }).catch(err => { |
|
118 |
+ return Promise.reject(err); |
|
119 |
+ }); |
|
120 |
+ } |
|
121 |
+ |
|
122 |
+ isRefreshing = true; |
|
123 |
+ |
|
124 |
+ try { |
|
125 |
+ // 액세스 토큰 재발급 요청 |
|
126 |
+ const newToken = await refreshAccessToken(); |
|
127 |
+ |
|
128 |
+ // 성공적으로 토큰을 재발급받은 경우 |
|
129 |
+ originalReq.headers['Authorization'] = newToken; |
|
130 |
+ |
|
131 |
+ // 대기 중인 요청 처리 |
|
132 |
+ processQueue(null, newToken); |
|
133 |
+ |
|
134 |
+ // 재요청 |
|
135 |
+ return apiClient(originalReq); |
|
136 |
+ } catch (refreshError) { |
|
137 |
+ // 대기 중인 모든 요청에 오류 전파 |
|
138 |
+ processQueue(refreshError, null); |
|
139 |
+ return Promise.reject(refreshError); |
|
140 |
+ } finally { |
|
141 |
+ isRefreshing = false; |
|
142 |
+ } |
|
143 |
+ } |
|
144 |
+ |
|
145 |
+ return Promise.reject(error); |
|
146 |
+ } |
|
147 |
+); |
|
148 |
+ |
|
149 |
+export default apiClient;(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/api/main.js
... | ... | @@ -0,0 +1,11 @@ |
1 | +import apiClient from "./index"; | |
2 | + | |
3 | +// 메인화면 정보 조회 | |
4 | +export const findAllSttusesProc = () => { | |
5 | + return apiClient.get(`/main/findAllSttuses.json`); | |
6 | +} | |
7 | + | |
8 | +// 통합 검색 | |
9 | +export const findAllDatas = (searchReqDTO) => { | |
10 | + return apiClient.get(`/main/findAllDatas.json`, { params: { searchReqDTO: JSON.stringify(searchReqDTO) } }); | |
11 | +}(파일 끝에 줄바꿈 문자 없음) |
+++ client/resources/js/youtubeUtils.js
... | ... | @@ -0,0 +1,50 @@ |
1 | +// youtubeUtils.js | |
2 | + | |
3 | +/** | |
4 | + * YouTube URL에서 비디오 ID를 추출합니다. | |
5 | + */ | |
6 | +export function extractYouTubeVideoId(url) { | |
7 | + if (!url) return null; | |
8 | + | |
9 | + // youtu.be/ID 형식 (단축 URL) | |
10 | + const shortUrlRegex = /youtu\.be\/([a-zA-Z0-9_-]{11})/; | |
11 | + const shortMatch = url.match(shortUrlRegex); | |
12 | + if (shortMatch) { | |
13 | + return shortMatch[1]; | |
14 | + } | |
15 | + | |
16 | + // youtube.com/watch?v=ID 형식 (일반 URL) | |
17 | + const standardRegex = /youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})/; | |
18 | + const standardMatch = url.match(standardRegex); | |
19 | + if (standardMatch) { | |
20 | + return standardMatch[1]; | |
21 | + } | |
22 | + | |
23 | + // youtube.com/embed/ID 형식 (임베드 URL) | |
24 | + const embedRegex = /youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/; | |
25 | + const embedMatch = url.match(embedRegex); | |
26 | + if (embedMatch) { | |
27 | + return embedMatch[1]; | |
28 | + } | |
29 | + | |
30 | + // 만약 위 정규식들이 실패하면 마지막 시도 | |
31 | + const lastAttemptRegex = /([a-zA-Z0-9_-]{11})/; | |
32 | + const urlParts = url.split(/[\/\?&]/); | |
33 | + for (const part of urlParts) { | |
34 | + if (part.match(lastAttemptRegex) && part.length === 11) { | |
35 | + return part; | |
36 | + } | |
37 | + } | |
38 | + | |
39 | + return null; | |
40 | +} | |
41 | + | |
42 | +/** | |
43 | + * YouTube 썸네일 URL을 가져옵니다 (가장 안정적인 방법). | |
44 | + */ | |
45 | +export function getYouTubeThumbnail(url) { | |
46 | + const videoId = extractYouTubeVideoId(url); | |
47 | + if (!videoId) return ''; | |
48 | + // return `https://i.ytimg.com/vi/${videoId}/mqdefault.jpg`; // 기본 해상도 | |
49 | + return `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`; // 고해상도 (지원 안되는 영상 있음) | |
50 | +}(파일 끝에 줄바꿈 문자 없음) |
--- client/views/pages/main/Main.vue
+++ client/views/pages/main/Main.vue
... | ... | @@ -1,31 +1,27 @@ |
1 | 1 |
<template> |
2 | 2 |
<div class="visual mb-50"> |
3 |
- |
|
4 |
- <swiper |
|
5 |
- ref="swiper" |
|
6 |
- :loop="true" |
|
7 |
- :spaceBetween="30" |
|
8 |
- :centeredSlides="true" |
|
9 |
- :autoplay="{ |
|
3 |
+ <swiper ref="swiper" :loop="true" :spaceBetween="30" :centeredSlides="true" :autoplay="{ |
|
10 | 4 |
delay: 2500, |
11 | 5 |
disableOnInteraction: false, |
12 |
- }" |
|
13 |
- :pagination="{ |
|
6 |
+ }" :pagination="{ |
|
14 | 7 |
type: 'progressbar', |
15 |
- }" :navigation="true" :modules="modules" @slideChange="onSlideChange" class="mySwiper "> |
|
8 |
+ }" :navigation="true" :modules="modules" @slideChange="onSlideChange" class="mySwiper"> |
|
16 | 9 |
<swiper-slide v-for="(item, index) in slides" :key="index"> |
17 | 10 |
<img :src="item.img" :alt="item.alt" /> |
18 | 11 |
</swiper-slide> |
19 | 12 |
<div class="pagination"> |
20 |
- <div class="page-count">{{ currentSlide }} / {{ totalSlides }}</div> |
|
21 |
- <div class="btn-control"> |
|
22 |
- <button @click="play"><CaretRightOutlined /></button> |
|
23 |
- <button @click="stop"><PauseOutlined /></button> |
|
13 |
+ <div class="page-count">{{ currentSlide }} / {{ totalSlides }}</div> |
|
14 |
+ <div class="btn-control"> |
|
15 |
+ <button @click="play"> |
|
16 |
+ <CaretRightOutlined /> |
|
17 |
+ </button> |
|
18 |
+ <button @click="stop"> |
|
19 |
+ <PauseOutlined /> |
|
20 |
+ </button> |
|
21 |
+ </div> |
|
24 | 22 |
</div> |
25 |
- </div> |
|
26 | 23 |
</swiper> |
27 | 24 |
<div class="search-wrap"> |
28 |
- |
|
29 | 25 |
<div class="search-area"> |
30 | 26 |
<select name="" id=""> |
31 | 27 |
<option value="all" selected>전체</option> |
... | ... | @@ -39,83 +35,50 @@ |
39 | 35 |
<button class="search-btn"><img :src="search" alt=""></button> |
40 | 36 |
</div> |
41 | 37 |
<div class="total-search"> |
42 |
- <button class="total-btn"><router-link :to="{path : '/TotalSearch.page'}" >상세검색</router-link></button> |
|
38 |
+ <button class="total-btn" @click="fnMoveTo('TotalSearch')"><a href="">상세검색</a></button> |
|
43 | 39 |
</div> |
44 | 40 |
</div> |
45 | 41 |
</div> |
46 | 42 |
<div class="current-status mb-60"> |
47 | 43 |
<div class="board w1500"> |
48 | 44 |
<ul> |
49 |
- <li> |
|
50 |
- <div class="labeling"><img :src="icon1" alt=""><span>전체</span></div> |
|
51 |
- <div class="count all">520</div> |
|
52 |
- </li> |
|
53 |
- <li class="line"></li> |
|
54 |
- <li> |
|
55 |
- <div class="labeling"><img :src="icon2" alt=""><span>사진</span></div> |
|
56 |
- <div class="count">520</div> |
|
57 |
- </li> |
|
58 |
- <li class="line"></li> |
|
59 |
- <li> |
|
60 |
- <div class="labeling"><img :src="icon3" alt=""><span>영상</span></div> |
|
61 |
- <div class="count">520</div> |
|
62 |
- </li> |
|
63 |
- <li class="line"></li> |
|
64 |
- <li> |
|
65 |
- <div class="labeling"><img :src="icon4" alt=""><span>미디어</span></div> |
|
66 |
- <div class="count">520</div> |
|
67 |
- </li> |
|
68 |
- <li class="line"></li> |
|
69 |
- <li> |
|
70 |
- <div class="labeling"><img :src="icon5" alt=""><span>보도자료</span></div> |
|
71 |
- <div class="count">520</div> |
|
72 |
- </li> |
|
45 |
+ <template v-for="(item, idx) of icons" :key="idx"> |
|
46 |
+ <li> |
|
47 |
+ <div class="labeling"><img :src="item.url" :alt="item.name + '아이콘'"><span>{{ item.name }}</span></div> |
|
48 |
+ <div :class="{ 'count': true, 'all': item.id === 'TOTAL' }">{{ item.rowCo }}</div> |
|
49 |
+ </li> |
|
50 |
+ <li class="line" v-if="idx < icons.length - 1"></li> |
|
51 |
+ </template> |
|
73 | 52 |
</ul> |
74 |
- <div class="current-btn"> <button><span>기록물 현황</span><img :src="direct" alt=""></button></div> |
|
53 |
+ <div class="current-btn"><button><span>기록물 현황</span><img :src="direct" alt=""></button></div> |
|
75 | 54 |
</div> |
76 | 55 |
</div> |
77 | 56 |
<div class="new-update w1500 mb-50"> |
78 | 57 |
<div class="tabs"> |
79 | 58 |
<ul class=""> |
80 |
- <li v-for="(tab, index) in tabs" :key="index" class="tab-title" :class="{ active: selectedTab === index }" |
|
81 |
- @click="selectTab(index)"> |
|
82 |
- <img :src="selectedTab === index ? tab.activeImage : tab.inactiveImage" :alt="tab.title" class="tab-icon" /> |
|
59 |
+ <li v-for="(tab, index) in tabs" :key="index" class="tab-title" :class="{ active: selectedTab === tab.id }" @click="selectTab(tab.id)"> |
|
60 |
+ <img :src="selectedTab === tab.id ? tab.activeImage : tab.inactiveImage" :alt="tab.title" class="tab-icon" /> |
|
83 | 61 |
<p><b>{{ tab.title }}</b> 기록물</p> |
84 | 62 |
</li> |
85 | 63 |
</ul> |
86 |
- |
|
87 | 64 |
<div class="tab-content"> |
88 |
- <div v-if="selectedTab === 0" class="content-wrap"> |
|
89 |
- <router-link :to="{ path: '/' }" class="gopage">더보기</router-link> |
|
90 |
- <div class="new-pic"> |
|
91 |
- <div v-for="(contentItem, index) in tabs[0].contentArray" :key="index" class="box-wrap"> |
|
92 |
- <div class="box"> |
|
93 |
- <img :src="contentItem.image" :alt="tabs[0].title" class="tab-image" /> |
|
94 |
- <div class="info"> |
|
95 |
- <p>{{ contentItem.content }}</p> |
|
96 |
- <span>{{ contentItem.date }}</span> |
|
65 |
+ <template v-for="(tabContent, idx1) of tabContents" :key="idx1"> |
|
66 |
+ <div v-show="tabContent.id === selectedTab" class="content-wrap"> |
|
67 |
+ <router-link :to="{ path: '/' }" class="gopage">더보기</router-link> |
|
68 |
+ <div class="new-pic"> |
|
69 |
+ <div v-for="(item, idx2) in tabContent.list" :key="idx2" class="box-wrap"> |
|
70 |
+ <div class="box"> |
|
71 |
+ <img :src="item.hasOwnProperty('files') && item.files.length > 0 ? item.files[0].filePath : null" :alt="item.sj" class="tab-image" /> |
|
72 |
+ <div class="info"> |
|
73 |
+ <p>{{ item.sj }}</p> |
|
74 |
+ <span>{{ item.rgsde.slice(0, -6) }}</span> |
|
75 |
+ </div> |
|
97 | 76 |
</div> |
98 | 77 |
</div> |
99 | 78 |
</div> |
100 | 79 |
</div> |
101 |
- </div> |
|
102 |
- <div v-if="selectedTab === 1" class="content-wrap"> |
|
103 |
- <router-link :to="{ path: '/' }" class="gopage">더보기</router-link> |
|
104 |
- <div class="new-video"> |
|
105 |
- <div v-for="(contentItem, index) in tabs[1].contentArray" :key="index" class="box-wrap"> |
|
106 |
- <div class="box"> |
|
107 |
- <img :src="contentItem.image" :alt="tabs[1].title" class="tab-image" /> |
|
108 |
- <div class="info"> |
|
109 |
- <p>{{ contentItem.content }}</p> |
|
110 |
- <span>{{ contentItem.date }}</span> |
|
111 |
- </div> |
|
112 |
- </div> |
|
113 |
- </div> |
|
114 |
- </div> |
|
115 |
- </div> |
|
80 |
+ </template> |
|
116 | 81 |
</div> |
117 |
- |
|
118 |
- |
|
119 | 82 |
</div> |
120 | 83 |
</div> |
121 | 84 |
<div class="new-update w1500 mb-50 flex-sp-bw"> |
... | ... | @@ -125,11 +88,11 @@ |
125 | 88 |
<router-link :to="{ path: '/' }" class="gopage">더보기</router-link> |
126 | 89 |
</div> |
127 | 90 |
<div class="media-wrap"> |
128 |
- <div v-for="(mediacontent, index) in mediacontent" :key="index" class="media-box"> |
|
129 |
- <img :src="mediacontent.image" :alt="mediacontent.title" class="media-image" /> |
|
91 |
+ <div v-for="(mediaContent, index) in mediaContent" :key="index" class="media-box"> |
|
92 |
+ <img :src="mediaContent.url" :alt="mediaContent.title" class="media-image" /> |
|
130 | 93 |
<div class="info"> |
131 |
- <p>{{ mediacontent.content }}</p> |
|
132 |
- <span>{{ mediacontent.date }}</span> |
|
94 |
+ <p>{{ mediaContent.content }}</p> |
|
95 |
+ <span>{{ mediaContent.date }}</span> |
|
133 | 96 |
</div> |
134 | 97 |
</div> |
135 | 98 |
</div> |
... | ... | @@ -140,17 +103,17 @@ |
140 | 103 |
<router-link :to="{ path: '/' }" class="gopage">더보기</router-link> |
141 | 104 |
</div> |
142 | 105 |
<ul> |
143 |
- <li v-for="(bodocontent, index) in bodocontent" :key="index" class="info"> |
|
144 |
- <p>{{ bodocontent.content }}</p> |
|
145 |
- <span>{{ bodocontent.date }}</span> |
|
106 |
+ <li v-for="(bodoContent, index) in bodoContent" :key="index" class="info"> |
|
107 |
+ <p>{{ bodoContent.content }}</p> |
|
108 |
+ <span>{{ bodoContent.date }}</span> |
|
146 | 109 |
</li> |
147 | 110 |
</ul> |
148 | 111 |
</div> |
149 | 112 |
</div> |
150 | 113 |
</template> |
151 |
- |
|
152 | 114 |
<script> |
153 | 115 |
import { ref } from 'vue'; |
116 |
+ |
|
154 | 117 |
// Import Swiper Vue components |
155 | 118 |
import { Swiper, SwiperSlide } from 'swiper/vue'; |
156 | 119 |
|
... | ... | @@ -161,9 +124,15 @@ |
161 | 124 |
import 'swiper/css/navigation'; |
162 | 125 |
import { CaretRightOutlined, PauseOutlined } from '@ant-design/icons-vue'; |
163 | 126 |
|
164 |
- |
|
165 | 127 |
// import required modules |
166 | 128 |
import { Autoplay, Pagination, Navigation } from 'swiper/modules'; |
129 |
+ |
|
130 |
+// 메인화면 조회 |
|
131 |
+import { findAllSttusesProc } from "../../../resources/api/main"; |
|
132 |
+ |
|
133 |
+// 유투브 유틸 |
|
134 |
+import { getYouTubeThumbnail } from '../../../resources/js/youtubeUtils'; |
|
135 |
+ |
|
167 | 136 |
export default { |
168 | 137 |
components: { |
169 | 138 |
Swiper, |
... | ... | @@ -178,45 +147,53 @@ |
178 | 147 |
}, |
179 | 148 |
data() { |
180 | 149 |
return { |
181 |
- selectedTab: 0, // Set initial tab index to 0 (first tab) |
|
150 |
+ selectedTab: "newPhoto", // Set initial tab index to 신규사진기록물 (first tab) |
|
182 | 151 |
tabs: [ |
183 | 152 |
{ |
184 |
- title: "신규 사진", activeImage: "client/resources/images/mCont_ico1_on.png", // Active tab image |
|
153 |
+ id: "newPhoto", |
|
154 |
+ title: "신규 사진", |
|
155 |
+ activeImage: "client/resources/images/mCont_ico1_on.png", // Active tab image |
|
185 | 156 |
inactiveImage: "client/resources/images/mCont_ico1_off.png", |
186 |
- contentArray: [ |
|
187 |
- { content: '문수사', date: '2025-03-18', image: 'client/resources/images/img1.png' }, |
|
188 |
- { content: '신평벽화마을', date: '2025-03-19', image: 'client/resources/images/img2.png' }, |
|
189 |
- { content: '박정희대통령역사자료관', date: '2025-03-20', image: 'client/resources/images/img3.png' } |
|
190 |
- ], |
|
191 | 157 |
}, |
192 | 158 |
{ |
193 |
- title: "신규 영상", activeImage: "client/resources/images/mCont_ico2_on.png", // Active tab image |
|
159 |
+ id: "newVideo", |
|
160 |
+ title: "신규 영상", |
|
161 |
+ activeImage: "client/resources/images/mCont_ico2_on.png", // Active tab image |
|
194 | 162 |
inactiveImage: "client/resources/images/mCont_ico2_off.png", |
195 |
- contentArray: [ |
|
196 |
- { content: '제목1', date: '2025-03-18', image: 'client/resources/images/img1.png' }, |
|
197 |
- { content: '제목2', date: '2025-03-19', image: 'client/resources/images/img2.png' }, |
|
198 |
- { content: '제목3', date: '2025-03-20', image: 'client/resources/images/img3.png' } |
|
199 |
- ], |
|
200 | 163 |
}, |
201 | 164 |
], |
202 |
- mediacontent: [ |
|
203 |
- { content: '문수사', date: '2025-03-18', image: 'client/resources/images/img4.png' }, |
|
204 |
- { content: '신평벽화마을', date: '2025-03-19', image: 'client/resources/images/img5.png' }, |
|
205 |
- ], |
|
206 |
- bodocontent: [ |
|
207 |
- { content: '구미시장 우리동네 온(溫)데이', date: '2025-03-18', }, |
|
208 |
- { content: "구미 청년 창업, 지난해 성과 '쏠쏠'…올해도 새 도전자", date: '2025-03-19', }, |
|
209 |
- { content: "구미시여성단체협의회 신경은 회장 부부, 1,000만 원", date: '2025-03-19', }, |
|
210 |
- { content: "초보 기술 공무원도 전문가로! 구미시, 실무 역량 향상", date: '2025-03-19', }, |
|
211 |
- { content: "구미시, 청렴도 1등급 목표…반부패·청렴 시책 강화", date: '2025-03-19', }, |
|
212 |
- ], |
|
165 |
+ tabContents: [], // 신규 사진, 영상 기록물 |
|
166 |
+ mediaContent: [], // 신규 미디어 영상 |
|
167 |
+ bodoContent: [], // 신규 보도자료 |
|
213 | 168 |
direct: 'client/resources/images/direct-btn.png', |
214 | 169 |
search: 'client/resources/images/icon/search.png', |
215 |
- icon1: 'client/resources/images/icon/icon1.png', |
|
216 |
- icon2: 'client/resources/images/icon/icon2.png', |
|
217 |
- icon3: 'client/resources/images/icon/icon3.png', |
|
218 |
- icon4: 'client/resources/images/icon/icon4.png', |
|
219 |
- icon5: 'client/resources/images/icon/icon5.png', |
|
170 |
+ icons: [ |
|
171 |
+ { |
|
172 |
+ id: "TOTAL", |
|
173 |
+ name: "전체", |
|
174 |
+ url: 'client/resources/images/icon/icon1.png', |
|
175 |
+ }, |
|
176 |
+ { |
|
177 |
+ id: "dcry_photo", |
|
178 |
+ name: "사진", |
|
179 |
+ url: 'client/resources/images/icon/icon2.png', |
|
180 |
+ }, |
|
181 |
+ { |
|
182 |
+ id: "dcry_vido", |
|
183 |
+ name: "영상", |
|
184 |
+ url: 'client/resources/images/icon/icon3.png', |
|
185 |
+ }, |
|
186 |
+ { |
|
187 |
+ id: "media_vido", |
|
188 |
+ name: "미디어", |
|
189 |
+ url: 'client/resources/images/icon/icon4.png', |
|
190 |
+ }, |
|
191 |
+ { |
|
192 |
+ id: "nes_dta", |
|
193 |
+ name: "보도자료", |
|
194 |
+ url: 'client/resources/images/icon/icon5.png', |
|
195 |
+ }, |
|
196 |
+ ], |
|
220 | 197 |
slides: [ |
221 | 198 |
{ img: 'client/resources/images/visual.png', alt: 'Slide 1' }, |
222 | 199 |
{ img: 'client/resources/images/visual.png', alt: 'Slide 2' }, |
... | ... | @@ -225,9 +202,12 @@ |
225 | 202 |
], |
226 | 203 |
currentSlide: 1, // To track the current slide |
227 | 204 |
totalSlides: 3, |
228 |
- |
|
205 |
+ |
|
229 | 206 |
navigation: true, // Enable navigation buttons (prev/next) |
230 | 207 |
}; |
208 |
+ }, |
|
209 |
+ created() { |
|
210 |
+ this.fnFindAllSttuses(); |
|
231 | 211 |
}, |
232 | 212 |
mounted() { |
233 | 213 |
// Store the swiper instance when it's ready |
... | ... | @@ -261,8 +241,68 @@ |
261 | 241 |
this.currentSlide = swiper.realIndex + 1; // Get current slide (1-based index) |
262 | 242 |
} |
263 | 243 |
}, |
244 |
+ // 메인화면 정보 조회 |
|
245 |
+ async fnFindAllSttuses() { |
|
246 |
+ try { |
|
247 |
+ const response = await findAllSttusesProc(); |
|
248 |
+ if (response.data.status == 200) { |
|
249 |
+ let dcrySttuses = response.data.data.dcrySttuses; |
|
250 |
+ dcrySttuses.forEach(item => { |
|
251 |
+ const matchingIcon = this.icons.find(icon => icon.id === item.iemNm); |
|
252 |
+ if (matchingIcon) matchingIcon.rowCo = item.rowCo; |
|
253 |
+ }); |
|
254 |
+ |
|
255 |
+ let photoDcrys = response.data.data.photoDcrys; |
|
256 |
+ this.tabContents.push({ id: 'newPhoto', list: photoDcrys }); |
|
257 |
+ |
|
258 |
+ let vidoDcrys = response.data.data.vidoDcrys; |
|
259 |
+ this.tabContents.push({ id: 'newVideo', list: vidoDcrys }); |
|
260 |
+ |
|
261 |
+ let mediaVidos = response.data.data.mediaVidos; |
|
262 |
+ this.mediaContent = this.onChangeList(mediaVidos); |
|
263 |
+ |
|
264 |
+ let nesDtas = response.data.data.nesDtas; |
|
265 |
+ this.bodoContent = this.onChangeList(nesDtas); |
|
266 |
+ } else { |
|
267 |
+ alert(response.data.message); |
|
268 |
+ } |
|
269 |
+ } catch (error) { |
|
270 |
+ alert("에러가 발생했습니다.\n시스템관리자에게 문의하세요."); |
|
271 |
+ } |
|
272 |
+ }, |
|
273 |
+ // 목록 변환 |
|
274 |
+ onChangeList(list) { |
|
275 |
+ let resultArr = []; |
|
276 |
+ |
|
277 |
+ for (let item of list) { |
|
278 |
+ let url = null; |
|
279 |
+ |
|
280 |
+ if (item.hasOwnProperty('files')) { |
|
281 |
+ if (item.files.length > 0) { |
|
282 |
+ url = item.files[0].filePath |
|
283 |
+ } |
|
284 |
+ } else { |
|
285 |
+ url = getYouTubeThumbnail(item.link); |
|
286 |
+ } |
|
287 |
+ |
|
288 |
+ resultArr.push({ |
|
289 |
+ content: item.sj, |
|
290 |
+ date: item.rgsde.slice(0, -6), |
|
291 |
+ url: url |
|
292 |
+ }); |
|
293 |
+ } |
|
294 |
+ |
|
295 |
+ return resultArr; |
|
296 |
+ }, |
|
297 |
+ |
|
298 |
+ // 페이지 이동 |
|
299 |
+ fnMoveTo(page) { |
|
300 |
+ this.$router.push({ |
|
301 |
+ name: page, |
|
302 |
+ query: {}, |
|
303 |
+ }); |
|
304 |
+ } |
|
264 | 305 |
}, |
265 | 306 |
}; |
266 | 307 |
</script> |
267 |
- |
|
268 | 308 |
<style scoped></style> |
--- server/modules/web/server.js
+++ server/modules/web/server.js
... | ... | @@ -140,4 +140,4 @@ |
140 | 140 |
response.redirect('/notFound.page'); // 에러 페이지로 유도 |
141 | 141 |
let message = `[Error:${errorCode}] ${request.url}/n ${error.stack}/n`; |
142 | 142 |
Logger.logging(message); |
143 |
-}); |
|
143 |
+});(파일 끝에 줄바꿈 문자 없음) |
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?