박정하 박정하 03-28
250328 박정하 영상기록물, 미디어영상 수정
@55b01383191f06f5df233ee80368faf67eec82aa
 
client/resources/api/mediaVido.js (added)
+++ client/resources/api/mediaVido.js
@@ -0,0 +1,26 @@
+import { apiClient } from "./index";
+
+// 미디어영상 목록 조회
+export const findAllMediaVidosProc = (searchReqDTO) => {
+  return apiClient.get(`/mediaVido/findAllMediaVidos.json`, { params: searchReqDTO });
+}
+
+// 미디어영상 상세 조회
+export const findMediaVidoProc = (mediaVidoId) => {
+  return apiClient.get(`/mediaVido/${mediaVidoId}/findMediaVido.json`);
+}
+
+// 미디어영상 등록
+export const saveMediaVidoProc = (mediaVidoInsertDTO) => {
+  return apiClient.post(`/mediaVido/saveMediaVido.json`, mediaVidoInsertDTO);
+}
+
+// 미디어영상 수정
+export const updateMediaVidoDcry = (mediaVidoUpdateDTO) => {
+  return apiClient.put(`/mediaVido/updateMediaVido.json`, mediaVidoUpdateDTO);
+}
+
+// 미디어영상 삭제
+export const deleteMediaVidoProc = (mediaVidoId) => {
+  return apiClient.put(`/mediaVido/${mediaVidoId}/deleteMediaVido.json`);
+}(파일 끝에 줄바꿈 문자 없음)
 
client/resources/api/nesDta.js (added)
+++ client/resources/api/nesDta.js
@@ -0,0 +1,26 @@
+import { apiClient, fileClient } from "./index";
+
+// 보도자료 목록 조회
+export const findAllNesDtasProc = (searchReqDTO) => {
+  return apiClient.get(`/nesDta/findAllNesDtas.json`, { params: searchReqDTO });
+}
+
+// 보도자료 상세 조회
+export const findNesDtaProc = (nesDtaId) => {
+  return apiClient.get(`/nesDta/${nesDtaId}/findNesDta.json`);
+}
+
+// 보도자료 등록
+export const saveNesDtaProc = (formData) => {
+  return fileClient.post(`/nesDta/saveNesDta.file`, formData);
+}
+
+// 보도자료 수정
+export const updateNesDtaProc = (formData) => {
+  return fileClient.put(`/nesDta/updateNesDta.file`, formData);
+}
+
+// 보도자료 삭제
+export const deleteNesDtaProc = (nesDtaId) => {
+  return apiClient.put(`/nesDta/${nesDtaId}/deleteNesDta.json`);
+}(파일 끝에 줄바꿈 문자 없음)
client/resources/js/youtubeUtils.js
--- client/resources/js/youtubeUtils.js
+++ client/resources/js/youtubeUtils.js
@@ -1,5 +1,3 @@
-// youtubeUtils.js
-
 /**
  * YouTube URL에서 비디오 ID를 추출합니다.
  */
client/views/component/listLayout/CardStyleComponent.vue
--- client/views/component/listLayout/CardStyleComponent.vue
+++ client/views/component/listLayout/CardStyleComponent.vue
@@ -4,12 +4,11 @@
       <div class="result-box">
         <div class="main-img">
           <img v-if="item.hasOwnProperty('files') && item.files.length > 0" :src="item.files[0].filePath" :alt="item.sj + ' 첫 번째 이미지'">
+          <img v-else-if="name === 'M'" :src="getYouTubeThumbnail(item.link)" alt="영상 썸네일">
           <img v-else src="client/resources/images/img6.png" alt="Not found image">
         </div>
         <div class="text-box">
-          <router-link :to="{ path: '/PicHistoryDetail.page' }">
-            <h5>{{ item.sj }}</h5>
-          </router-link>
+          <h5>{{ item.sj }}</h5>
           <p v-if="item.hasOwnProperty('adres')" class="address">{{ item.adres }}</p>
           <p class="text">{{ $stripHtml(item.cn) }}</p>
           <div class="mb-20">
@@ -30,13 +29,15 @@
   </ul>
 </template>
 <script>
+import { getYouTubeThumbnail } from '../../../resources/js/youtubeUtils';
+
 export default {
   name: "CardStyleComponent",
 
   props: {
     name: {
       type: String,
-      default: 'P',
+      default: 'P'
     },
     list: {
       type: Array,
@@ -60,6 +61,8 @@
   },
 
   methods: {
+    // 유투브 썸네일 제작
+    getYouTubeThumbnail,
     // 페이지 이동
     fnMoveTo(item) {
       let key = null;
 
client/views/component/player/VideoComponent.vue (added)
+++ client/views/component/player/VideoComponent.vue
@@ -0,0 +1,0 @@
 
client/views/component/player/YoutubeComponent.vue (added)
+++ client/views/component/player/YoutubeComponent.vue
@@ -0,0 +1,103 @@
+<template>
+  <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>
+  <div v-else style="text-align: center;">
+    <img src="client/resources/images/no_media.png" alt="미디어를 표시할 수 없습니다" style="width: auto; height: auto;" />
+  </div>
+</template>
+<script>
+export default {
+  name: 'YoutubePlayer',
+
+  props: {
+    link: {
+      type: String,
+      default: ''
+    }
+  },
+
+  data() {
+    return {
+      loadError: false
+    }
+  },
+
+  computed: {
+    videoId() {
+      if (!this.link) {
+        this.loadError = true;
+        return null;
+      }
+
+      try {
+        let id = null;
+
+        // 일반 YouTube URL (youtube.com/watch?v=VIDEO_ID)
+        if (this.link.includes('youtube.com/watch')) {
+          const url = new URL(this.link);
+          id = url.searchParams.get('v');
+        }
+
+        // 단축 URL (youtu.be/VIDEO_ID)
+        else if (this.link.includes('youtu.be/')) {
+          const parts = this.link.split('youtu.be/');
+          if (parts.length > 1) {
+            id = parts[1].split('?')[0].split('&')[0];
+          }
+        }
+
+        // 임베드 URL (youtube.com/embed/VIDEO_ID)
+        else if (this.link.includes('youtube.com/embed/')) {
+          const parts = this.link.split('youtube.com/embed/');
+          if (parts.length > 1) {
+            id = parts[1].split('?')[0].split('&')[0];
+          }
+        }
+
+        if (!id) {
+          this.loadError = true;
+          return null;
+        }
+
+        return id;
+      } catch (e) {
+        console.error('YouTube URL 파싱 오류:', e);
+        this.loadError = true;
+        return null;
+      }
+    },
+
+    embedUrl() {
+      if (!this.videoId) return '';
+      return `https://www.youtube-nocookie.com/embed/${this.videoId}?rel=0&modestbranding=1&origin=${encodeURIComponent(window.location.origin)}`;
+    }
+  },
+
+  methods: {
+    handleError() {
+      console.error('YouTube 비디오 로드 실패');
+      this.loadError = true;
+    }
+  },
+
+  mounted() {
+    if (this.videoId) {
+      const checkIframeLoaded = setTimeout(() => {
+        const iframe = this.$el.querySelector('iframe');
+        if (iframe) {
+          const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
+          if (!iframeDoc) {
+            this.loadError = true;
+          }
+        }
+        clearTimeout(checkIframeLoaded);
+      }, 5000); // 5초 타임아웃
+    }
+  },
+
+  watch: {
+    link() {
+      this.loadError = false;
+    }
+  }
+}
+</script>(파일 끝에 줄바꿈 문자 없음)
client/views/pages/user/MediaVideoDetail.vue
--- client/views/pages/user/MediaVideoDetail.vue
+++ client/views/pages/user/MediaVideoDetail.vue
@@ -1,148 +1,201 @@
 <template>
-    <div class="content">
-        <div class="sub-title-area mb-30">
-            <h2>미디어 영상</h2>
-            <div class="breadcrumb-list">
-                <ul>
-                    <li><img :src="homeicon" alt="Home Icon">
-                        <p>언론에서 바라본 구미시</p>
-                    </li>
-                    <li><img :src="righticon" alt=""></li>
-                    <li>미디어 영상</li>
-                </ul>
-            </div>
-        </div>
-        <form action="" class="gallery-form mb-40">
-            <dl class="mb-20">
-                <dd>
-                    <p>미디어 영상 제목1
-                    </p>
-                    <div class="date flex align-center">
-                        <img :src="calendaricon" alt="">
-                        <span>2025.02.28</span>
-                    </div>
-                </dd>
-
-            </dl>
-            <div class="gallery video">
-                    <img :src="eximg" alt="">
-                </div>
-        </form>
-
-        <h3>내용</h3>
-        <form action="" class=" info-form mb-50">
-            <dl>
-                <dd>
-                    <p> 대한민국 최대의 내륙 산업단지를 보유하고, 서울로부터 277km, 부산으로부터 167km 거리에 있으며, 면적은 615㎢로 경상북도 전체 면적의 3.2%에 달합니다. 인구는
-                        41만 명이고, 선산읍, 고아읍, 산동읍을 비롯한 3읍, 5면, 17개 동으로 구성되어 있습니다.</p>
-
-                </dd>
-            </dl>
-        </form>
-
-        <h3>기본정보</h3>
-        <form action="" class="info-form mb-50">
-            <dl>
-                <dd class="mb-20">
-                    <img :src="addressicon" alt="">
-                    <span>원본주소</span>
-                    <p>https://youtu.be/2F2gWkEnSz4</p>
-                </dd>
-                <dd class="mb-20">
-                    <img :src="yearicon" alt="">
-                    <span>생산연도</span>
-                    <p>2017</p>
-
-                </dd>
-                <dd>
-                    <img :src="categoryicon" alt="">
-                    <span>카테고리</span>
-                    <ul class="category">
-                        <li v-if="resultitem.category1" class="category1">카테고리1</li>
-                        <li v-if="resultitem.category2" class="category2">카테고리2</li>
-                    </ul>
-
-                </dd>
-
-            </dl>
-        </form>
-        <div class="btn-group flex-center">
-            <button class="red-line " type="button" @click="fnDeleteUser">삭제</button>
-            <button class="blue-line " type="button" @click="fnUpdateUser">수정</button>
-            <button class="gray-line-bg " type="button" @click="fnUpdateUser">목록</button>
-        </div>
+  <div class="content">
+    <div class="sub-title-area mb-30">
+      <h2>미디어 영상</h2>
+      <div class="breadcrumb-list">
+        <ul>
+          <li><img :src="homeicon" alt="Home Icon">
+            <p>언론에서 바라본 구미시</p>
+          </li>
+          <li><img :src="righticon" alt=""></li>
+          <li>미디어 영상</li>
+        </ul>
+      </div>
     </div>
+    <div class="gallery-form mb-40">
+      <dl class="mb-20">
+        <dd>
+          <p>{{ mediaVido.sj }} </p>
+          <div class="date flex align-center">
+            <img :src="calendaricon" alt="">
+            <span>{{ $dotFormatDate(mediaVido.rgsde) }}</span>
+          </div>
+        </dd>
+      </dl>
+      <div class="gallery video">
+        <Youtube-component :link="mediaVido.link" />
+      </div>
+    </div>
+    <h3>내용</h3>
+    <div class="info-form mb-50">
+      <dl>
+        <dd>
+          <Viewer-component :content="mediaVido.cn" />
+        </dd>
+      </dl>
+    </div>
+    <h3>기본정보</h3>
+    <div class="info-form mb-50">
+      <dl>
+        <dd class="mb-20">
+          <img :src="addressicon" alt="">
+          <span>원본주소</span>
+          <p>{{ mediaVido.link }}</p>
+        </dd>
+        <dd class="mb-20">
+          <img :src="yearicon" alt="">
+          <span>생산연도</span>
+          <p>{{ $dotFormatDate(mediaVido.prdctnYear) }}</p>
+        </dd>
+        <dd>
+          <img :src="categoryicon" alt="">
+          <span>카테고리</span>
+          <ul class="category">
+            <li v-for="(item, idx) of mediaVido.ctgrys" :key="idx" class="category">{{ item.ctgryNm }}</li>
+          </ul>
+        </dd>
+      </dl>
+    </div>
+    <div class="btn-group flex-center">
+      <button class="red-line " type="button" @click="fnDelete">삭제</button>
+      <button class="blue-line " type="button" @click="fnMoveTo('MediaVideoInsert', pageId)">수정</button>
+      <button class="gray-line-bg " type="button" @click="fnMoveTo('MediaVideoSearch')">목록</button>
+    </div>
+  </div>
 </template>
-
 <script>
-import axios from "axios";
-import { ref } from 'vue';
-import { updateUsers, logOutProc, updatePassword } from "../../../resources/api/user"
-// Import Swiper Vue components
-import { CaretRightOutlined, PauseOutlined } from '@ant-design/icons-vue';
-import { Swiper, SwiperSlide } from 'swiper/vue';
-
-// Import Swiper styles
-import 'swiper/css';
-
-import 'swiper/css/free-mode';
-import 'swiper/css/navigation';
-import 'swiper/css/thumbs';
-
-// import required modules
-import { FreeMode, Navigation, Thumbs } from 'swiper/modules';
+// COMPONENT
+import YoutubeComponent from '../../component/player/YoutubeComponent.vue';
+import ViewerComponent from '../../component/editor/ViewerComponent.vue';
+// API
+import { findMediaVidoProc, deleteMediaVidoProc } from '@/resources/api/mediaVido';
+import { fileDownloadProc } from '@/resources/api/file';
 
 export default {
-    components: {
-        PauseOutlined,
-        CaretRightOutlined,
-        Swiper,
-        SwiperSlide,
-    },
-    setup() {
-        const thumbsSwiper = ref(null);
+  components: {
+    YoutubeComponent,
+    ViewerComponent,
+  },
 
-        const setThumbsSwiper = (swiper) => {
-            thumbsSwiper.value = swiper;
-        };
+  data() {
+    return {
+      // ICON
+      calendaricon: 'client/resources/images/icon/calendaricon.png',
+      homeicon: 'client/resources/images/icon/home.png',
+      erroricon: 'client/resources/images/icon/error.png',
+      righticon: 'client/resources/images/icon/right.png',
+      addressicon: 'client/resources/images/icon/addressicon.png',
+      yearicon: 'client/resources/images/icon/yearicon.png',
+      categoryicon: 'client/resources/images/icon/categoryicon.png',
 
-        return {
-            thumbsSwiper,
-            setThumbsSwiper,
-            modules: [FreeMode, Navigation, Thumbs],
-        };
-    },
-    data() {
-        return {
-            resultitem: {
-                category1: true,
-                category2: true,
-            },
-            calendaricon: 'client/resources/images/icon/calendaricon.png',
-            homeicon: 'client/resources/images/icon/home.png',
-            erroricon: 'client/resources/images/icon/error.png',
-            righticon: 'client/resources/images/icon/right.png',
-            addressicon: 'client/resources/images/icon/addressicon.png',
-            yearicon: 'client/resources/images/icon/yearicon.png',
-            categoryicon: 'client/resources/images/icon/categoryicon.png',
-            eximg: 'client/resources/images/img8.png',
-            slides: [
-                { img: 'client/resources/images/visual.png', alt: 'Slide 1' },
-                { img: 'client/resources/images/visual.png', alt: 'Slide 2' },
-                { img: 'client/resources/images/visual.png', alt: 'Slide 3' },
-                { img: 'client/resources/images/visual.png', alt: 'Slide 3' },
-                { img: 'client/resources/images/visual.png', alt: 'Slide 3' },
-                { img: 'client/resources/images/visual.png', alt: 'Slide 3' },
-                // Add more slides as needed
-            ],
+      pageId: null,
+      mediaVido: {},
+      selectedFiles: [],
+    };
+  },
 
-        };
+  created() {
+    this.pageId = this.$route.query.id;
+    if (this.pageId === null) {
+      alert("게시물 존재하지 않습니다.");
+      this.fnMoveTo('MediaVideoSearch');
+    }
+  },
+
+  mounted() {
+    this.fnFindDcry(); // 상세 조회
+  },
+
+  methods: {
+    // 상세 조회
+    async fnFindDcry() {
+      try {
+        const response = await findMediaVidoProc(this.pageId);
+        this.mediaVido = response.data.data.mediaVido;
+      } catch (error) {
+        alert('조회중 오류가 발생했습니다.');
+        this.fnMoveTo('MediaVideoSearch');
+
+        if (error.response) {
+          alert(error.response.data.message);
+        }
+        console.error(error.message);
+      }
     },
-    methods: {
+
+    // 파일 다운로드
+    async fnDownload(type) {
+      // 유효성 검사
+      if (type === 'selected' && this.selectedFiles.length === 0) {
+        alert("파일을 1개 이상 선택하거나 전체 다운로드를 클릭해주세요.");
+        return;
+      }
+
+      let url = null;
+      let link = null;
+
+      try {
+        let fileIds = this.selectedFiles[0];
+        const response = await fileDownloadProc(fileIds);
+
+        // 파일명 조회
+        let filename = 'downloadFile.bin';
+        const filenameRegex = /file[Nn]ame[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
+        const matches = filenameRegex.exec(response.headers['content-disposition']);
+        if (matches != null && matches[1]) {
+          filename = matches[1].replace(/['"]/g, '');
+        }
+
+        // 파일 다운로드 생성
+        url = window.URL.createObjectURL(new Blob([response.data]));
+        link = document.createElement('a');
+        link.href = url;
+        link.setAttribute('download', filename);
+        document.body.appendChild(link);
+        link.click();
+      } catch (error) {
+        alert('파일 다운로드 중 오류가 발생했습니다.');
+      } finally {
+        // 리소스 정리
+        setTimeout(() => {
+          if (url) {
+            window.URL.revokeObjectURL(url);
+          }
+          if (link && link.parentNode) {
+            document.body.removeChild(link);
+          }
+        }, 100);
+      }
     },
-    watch: {},
-    computed: {
+
+    // 삭제
+    async fnDelete() {
+      let isCheck = confirm("해당 페이지를 삭제하시겠습니까?");
+      if (!isCheck) {
+        return;
+      }
+
+      try {
+        const response = await deleteMediaVidoProc(this.pageId);
+        alert('해당 페이지를 삭제했습니다.');
+
+        this.fnMoveTo('MediaVideoSearch');
+      } catch (error) {
+        if (error.response) {
+          alert(error.response.data.message);
+        }
+        console.error(error.message);
+      }
     },
-    mounted() { },
+
+    // 페이지 이동
+    fnMoveTo(page, id) {
+      if (this.$isEmpty(id)) {
+        this.$router.push({ name: page });
+      } else {
+        this.$router.push({ name: page, query: { id: id } });
+      }
+    },
+  },
 };
 </script>
(파일 끝에 줄바꿈 문자 없음)
client/views/pages/user/MediaVideoInsert.vue
--- client/views/pages/user/MediaVideoInsert.vue
+++ client/views/pages/user/MediaVideoInsert.vue
@@ -12,53 +12,57 @@
         </ul>
       </div>
     </div>
-    <form action="" class="insert-form mb-50">
+    <form class="insert-form mb-50">
       <dl>
         <dd>
-          <label for="id" class="require">제목</label>
-          <div class="wfull"><input type="text" id="id" placeholder="제목을 입력하세요."></div>
+          <label for="sj" class="require">제목</label>
+          <div class="wfull"><input type="text" id="sj" placeholder="제목을 입력하세요." v-model="requestDTO.sj"></div>
         </dd>
         <div class="hr"></div>
         <dd>
-          <label for="year">생산연도</label>
-          <input type="text" id="year" placeholder="생산연도를 입력하세요">
+          <label for="prdctnYear">생산연도</label>
+          <input type="text" id="prdctnYear" placeholder="생산연도를 입력하세요" v-model="requestDTO.prdctnYear">
         </dd>
         <div class="hr"></div>
         <dd>
-          <label for="address">주소</label>
-          <div class="wfull"><input type="text" id="address" placeholder="URL 주소를 입력하세요"></div>
+          <label for="link">주소</label>
+          <div class="wfull"><input type="text" id="link" placeholder="URL 주소를 입력하세요" v-model="requestDTO.link"></div>
         </dd>
         <div class="hr"></div>
         <dd>
           <label for="text">내용</label>
           <div class="wfull">
-            <EditorComponent :contents="insertDTO.cn" />
+            <EditorComponent v-model:contents="requestDTO.cn" />
           </div>
         </dd>
         <div class="hr"></div>
         <dd>
           <label for="category" class="flex align-center">
-            <p>카테고리</p><button type="button" class="category-add" @click="openModal">추가하기</button>
+            <p>카테고리</p><button type="button" class="category-add" @click="fnToggleModal">추가하기</button>
           </label>
           <ul class="category">
-            <li v-for="(category, index) in selectedCtgries" :key="index"> {{ category }} <button type="button" class="cancel" @click="removeCategory(index)"><b>✕</b></button>
-            </li>
+            <li v-for="(item, idx) of selectedCtgries" :key="idx">{{ item.ctgryNm }} <button type="button" class="cancel" @click="fnDelCtgry(item.ctgryId)"><b>✕</b></button></li>
           </ul>
         </dd>
       </dl>
     </form>
     <div class="btn-group flex-center">
-      <button class="cancel">취소</button>
-      <button class="register">등록</button>
+      <button type="button" class="cancel" @click="fnMoveTo('MediaVideoSearch')">취소</button>
+      <button type="button" class="register" @click="submitForm">
+        <span v-if="$isEmpty(pageId)">등록</span>
+        <span v-else>수정</span>
+      </button>
     </div>
   </div>
-  <CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" />
+  <CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" @addCtgries="fnAddCtgries" />
 </template>
 <script>
 import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
 // COMPONENT
 import EditorComponent from '../../component/editor/EditorComponent.vue';
 import CategorySelectModal from '../../component/modal/CategorySelectModal.vue';
+// API
+import { findMediaVidoProc, saveMediaVidoProc, updateMediaVidoDcry } from '@/resources/api/mediaVido';
 
 export default {
   components: {
@@ -66,116 +70,126 @@
     LeftOutlined,
     RightOutlined,
     DoubleRightOutlined,
-    EditorComponent, CategorySelectModal,
+    EditorComponent,
+    CategorySelectModal,
   },
 
   data() {
     return {
-      // Define the image sources
+      // 아이콘 경로
       homeicon: 'client/resources/images/icon/home.png',
       erroricon: 'client/resources/images/icon/error.png',
       righticon: 'client/resources/images/icon/right.png',
-      fileicon: 'client/resources/images/icon/file.png',
       searchicon: 'client/resources/images/icon/search.png',
 
-      isModalOpen: false,
+      pageId: null,
 
-      items: [
-        { id: 1, category: '카테고리 1', selected: false },
-        { id: 2, category: '카테고리 2', selected: false },
-        { id: 3, category: '카테고리 3', selected: false },
-      ],
-      fileNames: [],
-      insertDTO: {
-        sj: null, //제목
-        cn: null, //내용
-        adres: null, // 주소
+      isModalOpen: false,
+      isDragging: false,
+
+      // 등록/수정 요청 객체
+      requestDTO: {
+        mediaVidoId: null, // 미디어영상 아이디
+        sj: null, // 제목
+        cn: null, // 내용
+        link: null, // 주소
         prdctnYear: null, // 생산연도
-        ty: 'P', // 타입 ( P: 사진, V: 영상 )
-        multipartFiles: null, // 첨부파일 정보
-        ctgryIds: null, // 카테고리 정보
+        ctgryIds: [], // 카테고리 정보
       },
 
-      files: [],
       selectedCtgries: [], // 카테고리 목록
     };
   },
-  computed: {
-    filteredItems() {
-      // This could be modified to support filtering based on searchQuery
-      return this.items.filter(item =>
-        item.category.includes(this.searchQuery)
-      );
+
+  created() {
+    this.pageId = this.$route.query.id;
+    if (!this.$isEmpty(this.pageId)) {
+      this.fnFindMediaVido(); // 상세 조회
     }
   },
-  created() {
-  },
+
   methods: {
-    registerCategories() {
-      // Add selected categories to the displayed list
-      this.selectedCtgries = this.items
-        .filter(item => item.selected)
-        .map(item => item.category);
-      this.closeModal(); // Close modal after registration
-    },
-    removeCategory(index) {
-      // Remove category from the list
-      this.selectedCtgries.splice(index, 1);
-    },
-    searchCategories() {
-      // You can implement search logic if needed
-    },
-    nextPage() {
-      if (this.currentPage < this.totalPages) {
-        this.currentPage++;
+    // 상세 조회
+    async fnFindMediaVido() {
+      try {
+        const response = await findMediaVidoProc(this.pageId);
+        this.copyToMediaVidoReqDTO(response.data.data.mediaVido);
+      } catch (error) {
+        alert('조회중 오류가 발생했습니다.');
+        this.fnMoveTo('MediaVideoSearch'); // 목록으로 이동
+
+        if (error.response) {
+          alert(error.response.data.message);
+        }
+        console.error(error.message);
       }
     },
-    previousPage() {
-      if (this.currentPage > 1) {
-        this.currentPage--;
-      }
+
+    // mediaVido > requestDTO
+    copyToMediaVidoReqDTO(mediaVido) {
+      const copyFields = Object.keys(this.requestDTO);
+      copyFields.forEach(field => {
+        this.requestDTO[field] = this.$isEmpty(mediaVido[field]) ? null : mediaVido[field];
+      });
+      this.selectedCtgries = mediaVido.ctgrys.length > 0 ? mediaVido.ctgrys : [];
     },
-    showFileNames(event) {
-      const files = event.target.files;
-      this.fileNames = [];  // Clear previous file names
 
-      for (let i = 0; i < files.length; i++) {
-        const file = files[i];
-        const fileType = file.name.split('.').pop().toLowerCase();  // Get file extension
+    // 카테고리 모달 열기/닫기
+    fnToggleModal() {
+      this.isModalOpen = !this.isModalOpen;
+    },
 
-        // Set default icon
-        let iconPath = this.fileicons;
+    // 카테고리 등록
+    fnAddCtgries(selectedCtgries) {
+      this.selectedCtgries = [...this.selectedCtgries, ...selectedCtgries];
 
-        // Determine the icon based on file type
-        if (['jpg', 'jpeg', 'png', 'gif'].includes(fileType)) {
-          iconPath = 'client/resources/images/icon/imgicon.png';  // Example for image files
-        } else if (['pdf'].includes(fileType)) {
-          iconPath = 'client/resources/images/icon/pdficon.png';  // Example for PDF files
-        } else if (['xls'].includes(fileType)) {
-          iconPath = 'client/resources/images/icon/excelicon.png';  // Example for audio files
-        } else if (['hwp'].includes(fileType)) {
-          iconPath = 'client/resources/images/icon/hwpicon.png';  // Example for video files
+      this.fnToggleModal(); // 카테고리 모달 닫기
+    },
+
+    // 카테고리 삭제
+    fnDelCtgry(id) {
+      this.selectedCtgries = this.selectedCtgries.filter(item => item.ctgryId !== id);
+    },
+
+    // 등록
+    async submitForm() {
+      // 유효성 검사
+      if (!this.requestDTO.sj) {
+        alert("제목을 입력해 주세요.");
+        return;
+      }
+
+      try {
+        if (this.$isEmpty(this.pageId)) {
+          delete this.requestDTO.mediaVidoId;
         }
 
-        // Push the file name and corresponding icon to the fileNames array
-        this.fileNames.push({
-          name: file.name,
-          icon: iconPath
-        });
+        // requestDTO에 카테고리 Id 추가
+        this.requestDTO.ctgryIds = this.selectedCtgries.map(ctgry => ctgry.ctgryId);
+
+        // API 통신
+        const response = this.$isEmpty(this.pageId) ? await saveMediaVidoProc(this.requestDTO) : await updateMediaVidoDcry(this.requestDTO);
+        alert(this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다.");
+
+        this.fnMoveTo('MediaVideoDetail', response.data.data.mediaVidoId); // 상세 페이지로 이동
+      } catch (error) {
+        if (error.response) {
+          alert(error.response.data.message);
+        } else {
+          alert("에러가 발생했습니다.");
+        }
+        console.error(error.message);
+      };
+    },
+
+    // 페이지 이동
+    fnMoveTo(page, id) {
+      if (this.$isEmpty(id)) {
+        this.$router.push({ name: page });
+      } else {
+        this.$router.push({ name: page, query: { id: id } });
       }
-    },
-    removeFile(index) {
-      // Remove file from the list
-      this.fileNames.splice(index, 1);
-      console.log(removeFile)
-    },
-    openModal() {
-      this.isModalOpen = true;
-    },
-    // 모달 닫기
-    closeModal() {
-      this.isModalOpen = false;
-    },
+    }
   }
 };
-</script>
+</script>
(파일 끝에 줄바꿈 문자 없음)
client/views/pages/user/MediaVideoSearch.vue
--- client/views/pages/user/MediaVideoSearch.vue
+++ client/views/pages/user/MediaVideoSearch.vue
@@ -1,414 +1,284 @@
 <template>
-    <div class="content">
-        <div class="sub-title-area mb-30">
-            <h2>미디어 영상</h2>
-            <div class="breadcrumb-list">
-                <ul>
-                    <!-- Bind the image source dynamically for homeicon -->
-                    <li><img :src="homeicon" alt="Home Icon">
-                        <p>언론에서 바라본 구미시</p>
-                    </li>
-                    <li><img :src="righticon" alt=""></li>
-                    <li>미디어 영상</li>
-                </ul>
-            </div>
-        </div>
-        <div action="search" class="search-form form ">
-            <dl>
-                <dd class="mb-15">
-                    <p>검색범위</p>
-                    <ul>
-                        <li>
-                            <input type="checkbox" id="allScope" v-model="isChkAllScope"
-                                @change="fnChkAllOptions('scope')" />
-                            <label for="allScope">전체</label>
-                        </li>
-                        <li v-for="(scope, idx) in searchType" :key="idx">
-                            <input type="checkbox" :id="idx" :name="searchType" :value="scope.key"
-                                v-model="searchReqDTO.searchType" @change="fnChkOption('scope')" />
-                            <label :for="idx">{{ scope.value }}</label>
-                        </li>
-                    </ul>
-                </dd>
-                <dd class="mb-15">
-                    <p>검색어</p>
-                    <div class="wfull"><input type="text" v-model="searchReqDTO.searchText"></div>
-                </dd>
-                <dd class="mb-15">
-                    <p>생산연도</p>
-                    <input type="date" v-model="searchReqDTO.startYear">
-                    <p class="mark">~</p>
-                    <input type="date" v-model="searchReqDTO.endYear">
-                </dd>
-                <dd class="mb-20 category-dd">
-                    <p>카테고리</p>
-                    <ul>
-                        <li v-for="(category, idx) of categorys" :key="idx">
-                            <input type="checkbox" :id="category.ctgryId" name="categorys" :value="category.ctgryId"
-                                v-model="searchReqDTO.searchCtgry" />
-                            <label :for="category.ctgryId">{{ category.ctgryNm }}</label>
-                        </li>
-                    </ul>
-                </dd>
-                <dd class="mb-15">
-                    <p>정렬</p>
-                    <ul>
-                        <li v-for="(order, idx) of orders" :key="idx">
-                            <input type="radio" :id="order.key" name="orders" :value="order.key"
-                                v-model="searchReqDTO.order" />
-                            <label :for="order.key">{{ order.value }}</label>
-                        </li>
-                    </ul>
-                </dd>
-                <div class="btn-group">
-                    <button class="reset"><img :src="reseticon" alt="">
-                        <p>초기화</p>
-                    </button>
-                    <button class="search"><img :src="searchicon" alt="">
-                        <p>검색</p>
-                    </button>
-                </div>
-
-            </dl>
-
-        </div>
-        <div class="search-result">
-            <div class="tabs">
-                <div class="flex-sp-bw mb-20 align-center">
-                    <div class="resultext ">
-                        <img :src="resulticon" alt="">
-                        <p>총 <b>{{ count }}개</b>의 미디어 영상이 검색되었습니다. </p>
-                    </div>
-                    <div class="flex ">
-                        <ul class="tab-box mb-20">
-                            <li v-for="(tab, index) in tabs" :key="index" class="tab-title"
-                                :class="{ active: selectedTab === tab.id }" @click="selectTab(tab.id)">
-                                <img :src="selectedTab === tab.id ? tab.activeImage : tab.inactiveImage"
-                                    :alt="tab.title" class="tab-icon" />
-                                <p><b>{{ tab.title }}</b></p>
-                            </li>
-                        </ul>
-                        <div class="select-box">
-                            <select v-model="itemsPerPage" @change="changeItemsPerPage">
-                                <option :value="5" selected>5개</option>
-                                <option :value="10">10개</option>
-                                <option :value="15">15개</option>
-                            </select>
-                        </div>
-                    </div>
-
-                </div>
-
-                <div class="tab-content">
-                    <!-- Loop through tabContents, and only display content that matches selectedTab -->
-                    <div v-for="(tabContent, idx) in tabContents" :key="idx">
-                        <!-- Display content only if the tab's ID matches the selectedTab -->
-                        <div v-show="tabContent.id === selectedTab">
-                            <!-- 카드형 Section (Card Layout) -->
-                            <div v-if="tabContent.viewType === 'card'">
-                                <ul class="card-wrap">
-                                    <li v-for="(resultitem, index) in paginatedItems" :key="index" class="mb-30">
-                                        <div class="result-box">
-                                            <!-- Main Image Section -->
-                                            <div class="main-img">
-                                                <img :src="resultitem.img" alt="" class="tab-image" />
-                                            </div>
-                                            <!-- Text Section -->
-                                            <div class="text-box">
-                                                <router-link :to="{ path: '/MediaVideoDetail.page' }">
-                                                    <h5>{{ resultitem.title }}</h5>
-                                                </router-link>
-
-                                                <p class="address">{{ resultitem.address }}</p>
-                                                <p class="text">{{ resultitem.content }}</p>
-
-                                                <div class="mb-20">
-                                                    <ul class="category">
-                                                        <li v-if="resultitem.category1" class="category1">카테고리1</li>
-                                                        <li v-if="resultitem.category2" class="category2">카테고리2</li>
-                                                    </ul>
-                                                </div>
-
-                                                <div class="date">
-                                                    <ul>
-                                                        <li>생산연도 <b>{{ resultitem.year }}</b></li>
-                                                        <li>|</li>
-                                                        <li>등록 <b>{{ resultitem.date }}</b></li>
-                                                    </ul>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </li>
-                                </ul>
-
-                                <!-- Empty State if no results in paginatedItems -->
-                                <div v-if="paginatedItems.length === 0" class="no-results">
-                                    <p>등록된 게시물이 없습니다.</p>
-                                </div>
-                            </div>
-
-                            <!-- 리스트형 Section (List Layout) -->
-                            <div v-if="tabContent.viewType === 'list'">
-                                <ul class="list-wrap">
-                                    <li v-for="(resultitem, index) in paginatedItems" :key="index" class="mb-30">
-                                        <div class="text-box">
-                                            <router-link :to="{ path: '/MediaVideoDetail.page' }">
-                                                    <h5>{{ resultitem.title }}</h5>
-                                                </router-link>
-                                            <p class="address">{{ resultitem.address }}</p>
-
-                                            <div class="flex-sp-bw">
-                                                <div class="mb-20">
-                                                    <ul class="category">
-                                                        <li v-if="resultitem.category1" class="category1">카테고리1</li>
-                                                        <li v-if="resultitem.category2" class="category2">카테고리2</li>
-                                                    </ul>
-                                                </div>
-
-                                                <div class="date ">
-                                                    <ul>
-                                                        <li>생산연도 <b>{{ resultitem.year }}</b></li>
-                                                        <li>|</li>
-                                                        <li>등록 <b>{{ resultitem.date }}</b></li>
-                                                    </ul>
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </li>
-                                </ul>
-
-                                <!-- Empty State if no results in paginatedItems -->
-                                <div v-if="paginatedItems.length === 0" class="no-results">
-                                    <p>등록된 게시물이 없습니다.</p>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </div>
-
-            <div class="btn-group flex-end mt-40"><button class="register"> <router-link
-                        :to="{ path: '/MediaVideoInsert.page' }">등록</router-link></button></div>
-            <div class="pagination flex-center mt-40">
-
-                <!-- Previous and Next Page Buttons -->
-                <button>
-                    <DoubleLeftOutlined />
-                </button>
-                <button @click="previousPage" :disabled="currentPage === 1">
-                    <LeftOutlined />
-                </button>
-                <button class="page-number clicked">1</button>
-                <button @click="nextPage" :disabled="currentPage === totalPages">
-                    <RightOutlined />
-                </button>
-                <button>
-                    <DoubleRightOutlined />
-                </button>
-            </div>
-        </div>
+  <div class="content">
+    <div class="sub-title-area mb-30">
+      <h2>미디어 영상</h2>
+      <div class="breadcrumb-list">
+        <ul>
+          <li><img :src="homeicon" alt="Home Icon">
+            <p>언론에서 바라본 구미시</p>
+          </li>
+          <li><img :src="righticon" alt=""></li>
+          <li>미디어 영상</li>
+        </ul>
+      </div>
     </div>
-
+    <div class="search-form form">
+      <dl>
+        <dd class="mb-15">
+          <p>검색범위</p>
+          <ul>
+            <li>
+              <input type="checkbox" id="allScope" v-model="isChkAllScope" @change="fnChkAllOptions" />
+              <label for="allScope">전체</label>
+            </li>
+            <li>
+              <input type="checkbox" id="searchSj" v-model="searchReqDTO.useSj" @change="fnChkOption" />
+              <label for="searchSj">제목</label>
+            </li>
+            <li>
+              <input type="checkbox" id="searchCn" v-model="searchReqDTO.useCn" @change="fnChkOption" />
+              <label for="searchCn">내용</label>
+            </li>
+          </ul>
+        </dd>
+        <dd class="mb-15">
+          <p>검색어</p>
+          <div class="wfull"><input type="text" v-model="searchReqDTO.searchText" v-on:keyup.enter="fnSearch()"></div>
+        </dd>
+        <dd class="mb-15">
+          <p>생산연도</p>
+          <input type="date" v-model="searchReqDTO.startYear">
+          <p class="mark">~</p>
+          <input type="date" v-model="searchReqDTO.endYear">
+        </dd>
+        <dd class="mb-20 category-dd">
+          <p>카테고리</p>
+          <ul>
+            <li v-for="(category, idx) of categorys" :key="idx">
+              <input type="checkbox" :id="'ctgry_' + idx" name="categorys" :value="category.ctgryId" v-model="searchReqDTO.searchCtgries" />
+              <label :for="'ctgry_' + idx">{{ category.ctgryNm }}</label>
+            </li>
+          </ul>
+        </dd>
+        <dd class="mb-15">
+          <p>정렬</p>
+          <ul>
+            <li v-for="(order, idx) of orders" :key="idx">
+              <input type="radio" :id="order.key" name="orders" :value="order.key" v-model="searchReqDTO.order" />
+              <label :for="order.key">{{ order.value }}</label>
+            </li>
+          </ul>
+        </dd>
+        <div class="btn-group">
+          <button type="button" class="reset" @click="init">
+            <img :src="reseticon" alt="">
+            <p>초기화</p>
+          </button>
+          <button type="button" class="search" @click="fnSearch">
+            <img :src="searchicon" alt="">
+            <p>검색</p>
+          </button>
+        </div>
+      </dl>
+    </div>
+    <div class="search-result">
+      <div class="tabs">
+        <div class="flex-sp-bw mb-20 align-center">
+          <div class="resultext ">
+            <img :src="resulticon" alt="">
+            <p>총 <b>{{ searchReqDTO.totalRecordCount }}개</b>의 미디어 영상이 검색되었습니다. </p>
+          </div>
+          <div class="flex">
+            <ul class="tab-box mb-20">
+              <li v-for="(tab, idx) in tabs" :key="idx" class="tab-title" :class="{ active: selectedTabId === tab.id }" @click="selectTab(tab.id)">
+                <img :src="selectedTabId === tab.id ? tab.activeImage : tab.inactiveImage" :alt="tab.title" class="tab-icon" />
+                <p><b>{{ tab.title }}</b></p>
+              </li>
+            </ul>
+            <div class="select-box">
+              <select v-model="searchReqDTO.recordSize" @change="fnSearch">
+                <option :value="24">24개</option>
+                <option :value="36">36개</option>
+                <option :value="100">100개</option>
+              </select>
+            </div>
+          </div>
+        </div>
+        <div class="tab-content">
+          <div v-if="searchResult.length > 0">
+            <CardStyleComponent v-if="selectedTabId === 'CARD'" :name="'M'" :list="searchResult" />
+            <ListStyleComponent v-if="selectedTabId === 'LIST'" :name="'M'" :list="searchResult" />
+          </div>
+          <div v-else class="no-results">
+            <p>등록된 게시물이 없습니다.</p>
+          </div>
+        </div>
+      </div>
+      <div class="btn-group flex-end mt-40"><button class="register"> <router-link :to="{ path: '/MediaVideoInsert.page' }">등록</router-link></button></div>
+      <DefaultPagination class="mt-40" :search="searchReqDTO" @onChange="fnChangeCurrentPage" />
+    </div>
+  </div>
 </template>
 <script>
-import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
-import { findAllCategoryProc } from "../../../resources/api/category"; // 카테고리 목록 검색
+// COMPONENT
+import CardStyleComponent from '@/views/component/listLayout/CardStyleComponent.vue';
+import ListStyleComponent from '@/views/component/listLayout/ListStyleComponent.vue';
+import DefaultPagination from '@/views/component/DefaultPagination.vue';
+// API
+import { findAllByNullProc } from "@/resources/api/category";
+import { findAllMediaVidosProc } from "@/resources/api/mediaVido";
 
 export default {
-    components: {
-        DoubleLeftOutlined,
-        LeftOutlined,
-        RightOutlined,
-        DoubleRightOutlined,
-    },
-    data() {
-        return {
-            selectedTab: 1,
-            // 검색용 객체
-            searchReqDTO: {
-                searchType: [],
-                searchText: null,
-                startYear: null,
-                endYear: null,
-                searchTy: null,
-                searchCtgry: [],
-                order: "rgsde",
-            },
-            tabs: [
+  components: {
+    DefaultPagination,
+    CardStyleComponent,
+    ListStyleComponent,
+  },
 
-                {
-                    id: 1,
-                    title: "카드형",
-                    activeImage: "client/resources/images/list_icon01_on.png", // Active tab image
-                    inactiveImage: "client/resources/images/list_icon01_off.png",
-                },
-                {
-                    id: 2,
-                    title: "리스트형",
-                    activeImage: "client/resources/images/list_icon02_on.png", // Active tab image
-                    inactiveImage: "client/resources/images/list_icon02_off.png",
-                },
-            ],
-            tabContents: [
-                { id: 1, viewType: 'card', list: [{ sj: 'Item 1', rgsde: '2025-03-01', files: [{ filePath: 'image1.png' }] }] },
-                { id: 2, viewType: 'list', list: [{ sj: 'Item 2', rgsde: '2025-03-02', files: [{ filePath: 'image2.png' }] }] },
-            ],
-            paginatedItems: [],
-            resultitems: [
-                {
-                    img: 'client/resources/images/img6.png',
-                    title: '미디어 영상 제목',
-                    address: '경상북도 구미시 송정대로 55',
-                    content: '대한민국 최대의 내륙 산업단지를 보유하고, 서울로부터 277km, 부산으로부터 167km 거리에 있으며, 면적은 615㎢로 경상북도 전체 면적의 3.2%에 달합니다. 인구는 41만 명이고, 선산읍, 고아읍, 산동읍을 비롯한 3읍, 5면, 17개 동으로 구성되어…',
-                    category1: true,
-                    category2: true,
-                    year: 2020,
-                    date: '2021-01-01'
-                },
+  data() {
+    return {
+      // ICON
+      resulticon: "client/resources/images/icon/r-check.png",
+      homeicon: 'client/resources/images/icon/home.png',
+      searchicon: 'client/resources/images/icon/search.png',
+      reseticon: 'client/resources/images/icon/reset.png',
+      righticon: 'client/resources/images/icon/right.png',
 
-            ],
-            currentPage: 1, // Current page number
-            itemsPerPage: 5,
-            resulticon: "client/resources/images/icon/r-check.png",
-            homeicon: 'client/resources/images/icon/home.png',
-            searchicon: 'client/resources/images/icon/search.png',
-            reseticon: 'client/resources/images/icon/reset.png',
-            righticon: 'client/resources/images/icon/right.png',
-            count: 23,
-            checkOptions: [
-                '전체',
-                '사진',
-                '영상',
-                '미디어 영상',
-                '보도자료',
-            ],
-            checkOptions2: [
-                '전체',
-                '제목',
-                '내용',
-                '주소',
-            ],
-            checkOptions3: [
-                '카테고리1',
-                '카테고리2',
-                '카테고리3',
-                '카테고리4',
-                '카테고리5',
-            ],
-            checkOptions4: [
-                '최신',
-                '인기',
-            ],
-            isChkAllScope: false, // 검색범위 전체 체크 여부
-            searchType: [
-                { key: "sj", value: "제목" },
-                { key: "cn", value: "내용" },
-                { key: "adres", value: "주소" },
-            ], // 검색범위 목록
-            categorys: [], // 카테고리 목록
-            orders: [
-                { key: "rgsde", value: "최신" },
-                { key: "rdcnt", value: "인기" },
-            ], // 정렬 목록
-        };
-    },
-    computed: {
-        // Total number of pages
-        totalPages() {
-            return Math.ceil(this.resultitems.length / this.itemsPerPage);
-        },
+      // 검색용 객체
+      isChkAllScope: true, // 검색범위 전체 체크 여부
+      searchType: [
+        { key: "sj", value: "제목" },
+        { key: "cn", value: "내용" },
+        { key: "adres", value: "주소" },
+      ], // 검색범위 목록
+      categorys: [], // 카테고리 목록
+      orders: [
+        { key: "rgsde", value: "최신" },
+        { key: "rdcnt", value: "인기" },
+      ], // 정렬 목록
 
-        // Paginated items based on current page and items per page
-        paginatedItems() {
-            const start = (this.currentPage - 1) * this.itemsPerPage;
-            const end = start + this.itemsPerPage;
-            return this.resultitems.slice(start, end);
-        },
-    },
-    created() {
-        // 초기 데이터 세팅
-        this.isChkAllScope = true;
-        this.searchReqDTO.searchType = this.searchType.map(item => item.key);
-        this.searchReqDTO.order = this.orders[0].key
+      // 검색용 객체 초기값
+      searchDefault: {
+        useSj: true,
+        useCn: true,
+        searchText: null,
+        startYear: null,
+        endYear: null,
+        searchCtgries: [],
+        order: "rgsde",
+        // 페이지네이션
+        currentPage: 1, // 현재 페이지
+        recordSize: 24, // 한 페이지에 표시할 데이터 개수
+      },
+      searchReqDTO: {}, // 실제 검색에 사용되는 객체
 
-        this.fnFindCategorys(); // 카테고리 목록 조회 (검색조건 없음)
-    },
-    methods: {
-        selectTab(tabId) {
-            this.selectedTab = tabId; // Update the selected tab index
-        },
-        // Change the number of items displayed per page
-        changeItemsPerPage() {
-            this.currentPage = 1; // Reset to first page when changing items per page
-        },
-        previousPage() {
-            if (this.currentPage > 1) {
-                this.currentPage--;
-            }
-        },
+      searchResult: [], // 검색결과
 
-        // Go to the next page
-        nextPage() {
-            if (this.currentPage < this.totalPages) {
-                this.currentPage++;
-            }
+      // 목록 레이아웃
+      selectedTabId: null,
+      tabs: [
+        {
+          id: "CARD",
+          title: "카드형",
+          activeImage: "client/resources/images/list_icon01_on.png", // Active tab image
+          inactiveImage: "client/resources/images/list_icon01_off.png",
         },
-        isChkAllScope: false, // 검색범위 전체 체크 여부
-        searchType: [
-            { key: "sj", value: "제목" },
-            { key: "cn", value: "내용" },
-            { key: "adres", value: "주소" },
-        ], // 검색범위 목록
-        categorys: [], // 카테고리 목록
-        orders: [
-            { key: "rgsde", value: "최신" },
-            { key: "rdcnt", value: "인기" },
-        ], // 정렬 목록
-
-        async fnFindCategorys() {
-            try {
-                const response = await findAllCategoryProc();
-                this.categorys = response.data.data.ctgry;
-            } catch (error) {
-                if (error.response) {
-                    console.log("에러 응답:", error.response.data);
-                }
-                console.error("Error:", error);
-            }
+        {
+          id: "LIST",
+          title: "리스트형",
+          activeImage: "client/resources/images/list_icon02_on.png", // Active tab image
+          inactiveImage: "client/resources/images/list_icon02_off.png",
         },
+      ],
 
-        // 통합검색
-        async fnFindAllDatas() {
-            try {
-                let params = {};
-                if (this.searchReqDTO.searchRecord.length > 0) {
-                    params.searchRecords = this.searchReqDTO.searchRecord.join(',');
-                }
-                if (this.searchReqDTO.searchType.length > 0) {
-                    params.searchTypes = this.searchReqDTO.searchType.join(',');
-                }
-                if (this.searchReqDTO.searchCtgry.length > 0) {
-                    params.searchCtgries = this.searchReqDTO.searchCtgry.join(',');
-                }
-                params.searchText = this.searchReqDTO.searchText;
-                params.startYear = this.searchReqDTO.startYear;
-                params.endYear = this.searchReqDTO.endYear;
-                params.order = this.searchReqDTO.order;
+      isInitialLoad: true // 초기 로드 여부
+    };
+  },
 
-                // API 호출
-                const response = await findAllDatas(params);
-                this.searchResult = response.data.data.searchResult;
-            } catch (error) {
-                if (error.response) {
-                    console.log("에러 응답:", error.response.data);
-                }
-                console.error("Error:", error);
-            }
-        },
+  created() {
+    this.init(); // 초기화
+    this.fnFindCategorys(); // 카테고리 목록 조회 (검색조건 없음)
+  },
+
+  mounted() {
+    let searchText = this.$route.query.searchText;
+    if (searchText !== null) {
+      this.searchReqDTO.searchText = searchText;
+    }
+
+    this.fnSearch(); // 통합검색
+  },
+
+  methods: {
+    // 초기화
+    init() {
+      if (this.isInitialLoad) {
+        this.isInitialLoad = false;
+      } else {
+        if (!confirm('검색 조건을 초기화하시겠습니까?')) {
+          return;
+        }
+      }
+
+      this.searchReqDTO = JSON.parse(JSON.stringify(this.searchDefault));
+      this.searchResult = []; // 검색결과 초기화
+
+      this.selectedTabId = this.tabs[0].id;
+
+      this.fnSearch(); // 통합검색
     },
 
+    // 카테고리 목록 조회
+    async fnFindCategorys() {
+      try {
+        const response = await findAllByNullProc();
+        this.categorys = response.data.data.ctgry;
+      } catch (error) {
+        this.categorys = []; // 카테고리 목록 초기화
 
+        if (error.response) {
+          alert(error.response.data.message);
+        }
+        console.error(error.message);
+      }
+    },
 
+    // 페이지 이동
+    fnChangeCurrentPage(currentPage) {
+      this.searchReqDTO.currentPage = Number(currentPage);
+      this.fnFindCategorys();
+    },
+
+    // 통합검색
+    async fnSearch() {
+      try {
+        const params = JSON.parse(JSON.stringify(this.searchReqDTO));
+
+        // 카테고리 목록 처리
+        if (this.searchReqDTO.searchCtgries && this.searchReqDTO.searchCtgries.length > 0) {
+          params.searchCtgries = this.searchReqDTO.searchCtgries.join(',');
+        } else {
+          delete params.searchCtgries;
+        }
+
+        // API 호출
+        const response = await findAllMediaVidosProc(params);
+        this.searchResult = response.data.data.mediaVidos;
+        this.searchReqDTO = response.data.data.search;
+      } catch (error) {
+        this.searchResult = []; // 검색결과 초기화
+
+        if (error.response) {
+          alert(error.response.data.message);
+        }
+        console.error(error.message);
+      }
+    },
+
+    // 기록유형 전체 선택 여부 변경
+    fnChkAllOptions() {
+      this.searchReqDTO.useSj = this.isChkAllScope;
+      this.searchReqDTO.useCn = this.isChkAllScope;
+    },
+
+    // 기록유형 선택 여부 변경
+    fnChkOption() {
+      this.isChkAllScope = this.searchReqDTO.useSj && this.searchReqDTO.useCn;
+    },
+
+    selectTab(tabId) {
+      this.selectedTabId = tabId;
+    },
+  },
 };
-</script>
-<style scoped></style>
(파일 끝에 줄바꿈 문자 없음)
+</script>
(파일 끝에 줄바꿈 문자 없음)
client/views/pages/user/PicHistoryDetail.vue
--- client/views/pages/user/PicHistoryDetail.vue
+++ client/views/pages/user/PicHistoryDetail.vue
@@ -158,9 +158,16 @@
     async fnFindDcry() {
       try {
         const response = await findDcryProc(this.pageId);
+
+        if (response.data.data.dcry.ty !== 'P') {
+          alert('올바른 접근이 아닙니다.');
+          this.fnMoveTo('PicHistorySearch'); // 목록으로 이동
+        }
+
         this.dcry = response.data.data.dcry;
       } catch (error) {
         alert('조회중 오류가 발생했습니다.');
+        this.fnMoveTo('PicHistorySearch');
 
         if (error.response) {
           alert(error.response.data.message);
client/views/pages/user/PicHistoryInsert.vue
--- client/views/pages/user/PicHistoryInsert.vue
+++ client/views/pages/user/PicHistoryInsert.vue
@@ -160,6 +160,12 @@
     async fnFindDcry() {
       try {
         const response = await findDcryProc(this.pageId);
+
+        if (response.data.data.dcry.ty !== 'P') {
+          alert('올바른 접근이 아닙니다.');
+          this.fnMoveTo('VideoHistorySearch'); // 목록으로 이동
+        }
+
         this.copyToDcryReqDTO(response.data.data.dcry);
       } catch (error) {
         alert('조회중 오류가 발생했습니다.');
client/views/pages/user/VideoHistoryDetail.vue
--- client/views/pages/user/VideoHistoryDetail.vue
+++ client/views/pages/user/VideoHistoryDetail.vue
@@ -15,10 +15,10 @@
     <form action="" class="gallery-form mb-40">
       <dl class="mb-20">
         <dd>
-          <p>영상 기록물 제목1 </p>
+          <p>{{ dcry.sj }}</p>
           <div class="date flex align-center">
             <img :src="calendaricon" alt="">
-            <span>2025.02.28</span>
+            <span>{{ $dotFormatDate(dcry.rgsde) }}</span>
           </div>
         </dd>
       </dl>
client/views/pages/user/VideoHistoryInsert.vue
--- client/views/pages/user/VideoHistoryInsert.vue
+++ client/views/pages/user/VideoHistoryInsert.vue
@@ -12,37 +12,36 @@
         </ul>
       </div>
     </div>
-    <form action="" class="insert-form mb-50">
+    <form class="insert-form mb-50">
       <dl>
         <dd>
-          <label for="id" class="require">제목</label>
-          <div class="wfull"><input type="text" id="id" placeholder="제목을 입력하세요."></div>
+          <label for="sj" class="require">제목</label>
+          <div class="wfull"><input type="text" id="sj" placeholder="제목을 입력하세요." v-model="reqDTO.sj"></div>
         </dd>
         <div class="hr"></div>
         <dd>
-          <label for="year">생산연도</label>
-          <input type="text" id="year" placeholder="생산연도를 입력하세요">
+          <label for="prdctnYear">생산연도</label>
+          <input type="text" id="prdctnYear" placeholder="생산연도를 입력하세요" v-model="reqDTO.prdctnYear">
         </dd>
         <div class="hr"></div>
         <dd>
-          <label for="address">주소</label>
-          <div class="wfull"><input type="text" id="address" placeholder="주소를 입력하세요"></div>
+          <label for="adres">주소</label>
+          <div class="wfull"><input type="text" id="adres" placeholder="주소를 입력하세요" v-model="reqDTO.adres"></div>
         </dd>
         <div class="hr"></div>
         <dd>
           <label for="text">내용</label>
           <div class="wfull">
-            <EditorComponent :contents="insertDTO.cn" />
+            <EditorComponent v-model:contents="reqDTO.cn" />
           </div>
         </dd>
         <div class="hr"></div>
         <dd>
           <label for="category" class="flex align-center">
-            <p>카테고리</p><button type="button" class="category-add" @click="openModal">추가하기</button>
+            <p>카테고리</p><button type="button" class="category-add" @click="fnToggleModal">추가하기</button>
           </label>
           <ul class="category">
-            <li v-for="(category, index) in selectedCtgries" :key="index"> {{ category }} <button type="button" class="cancel" @click="removeCategory(index)"><b>✕</b></button>
-            </li>
+            <li v-for="(item, idx) of selectedCtgries" :key="idx">{{ item.ctgryNm }} <button type="button" class="cancel" @click="fnDelCtgry(item.ctgryId)"><b>✕</b></button></li>
           </ul>
         </dd>
         <div class="hr"></div>
@@ -51,26 +50,35 @@
           <ul class="wfull">
             <li class="flex align-center">
               <p>파일첨부</p>
-              <div class="invalid-feedback"><img :src="erroricon" alt=""><span>첨부파일은 10건까지 등록 가능하며, 건당 최대 100MB를 초과할 수 없습니다.</span></div>
+              <div class="invalid-feedback"><img :src="erroricon" alt=""><span>첨부파일은 건당 최대 10GB를 초과할 수 없습니다.</span></div>
             </li>
             <li class="file-insert">
-              <input type="file" id="fileInput" class="file-input" multiple @change="showFileNames">
-              <label for="fileInput" class="file-label mb-20">
-                <div class="flex-center align-center"><img :src="fileicon" alt="">
+              <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">
+              <label for="fileInput" class="file-label mb-20" @dragover.prevent="handleDragOver" @dragleave.prevent="handleDragLeave" @drop.prevent="handleDrop" :class="{ 'drag-over': isDragging }">
+                <div class="flex-center align-center">
+                  <img :src="fileicon" alt="">
                   <p>파일첨부하기</p>
                 </div>
                 <p>파일을 첨부하시려면 이 영역으로 파일을 끌고 오거나 클릭해주세요</p>
               </label>
               <p class="mb-10">파일목록</p>
               <div id="fileNames" class="file-names">
-                <span v-if="fileNames.length === 0">선택된 파일이 없습니다.</span>
-                <div v-for="(file, index) in fileNames" :key="index" class="flex-sp-bw mb-5 file-wrap">
+                <div v-if="reqDTO.files.length === 0 && multipartFiles.length === 0">선택된 파일이 없습니다.</div>
+                <!-- 새로 추가된 파일 목록 -->
+                <div v-for="(file, idx) of multipartFiles" :key="idx" class="flex-sp-bw mb-5 file-wrap">
                   <div class="file-name">
-                    <!-- Corrected here: Use file.icon instead of fileicons.img -->
-                    <img :src="file.icon" alt="fileicon">
+                    <img src="/client/resources/images/icon/imgicon.png" alt="fileicon">
                     <p>{{ file.name }}</p>
                   </div>
-                  <button type="button" class="cancel" @click="removeFile(index)"><b>✕</b></button>
+                  <button type="button" class="cancel" @click="fnDelFile('new', idx)"><b>✕</b></button>
+                </div>
+                <!-- 기존 등록된 파일 목록 -->
+                <div v-for="(file, idx) of reqDTO.files" :key="idx" class="flex-sp-bw mb-5 file-wrap">
+                  <div class="file-name">
+                    <img src="/client/resources/images/icon/imgicon.png" alt="fileicon">
+                    <p>{{ file.fileNm }}</p>
+                  </div>
+                  <button type="button" class="cancel" @click="fnDelFile('old', file.fileId)"><b>✕</b></button>
                 </div>
               </div>
             </li>
@@ -79,17 +87,22 @@
       </dl>
     </form>
     <div class="btn-group flex-center">
-      <button class="cancel">취소</button>
-      <button class="register">등록</button>
+      <button type="button" class="cancel" @click="fnMoveTo('VideoHistorySearch')">취소</button>
+      <button type="button" class="register" @click="submitForm">
+        <span v-if="$isEmpty(pageId)">등록</span>
+        <span v-else>수정</span>
+      </button>
     </div>
   </div>
-  <CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" />
+  <CategorySelectModal v-if="isModalOpen" :selectedCtgries="selectedCtgries" @toggleModal="fnToggleModal" @addCtgries="fnAddCtgries" />
 </template>
 <script>
 import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
 // COMPONENT
 import EditorComponent from '../../component/editor/EditorComponent.vue';
 import CategorySelectModal from '../../component/modal/CategorySelectModal.vue';
+// API
+import { findDcryProc, saveDcry, updateDcry } from '@/resources/api/dcry';
 
 export default {
   components: {
@@ -97,116 +110,265 @@
     LeftOutlined,
     RightOutlined,
     DoubleRightOutlined,
-    EditorComponent, CategorySelectModal,
+    EditorComponent,
+    CategorySelectModal,
   },
 
   data() {
     return {
-      // Define the image sources
+      // 아이콘 경로
       homeicon: 'client/resources/images/icon/home.png',
       erroricon: 'client/resources/images/icon/error.png',
       righticon: 'client/resources/images/icon/right.png',
       fileicon: 'client/resources/images/icon/file.png',
       searchicon: 'client/resources/images/icon/search.png',
 
-      isModalOpen: false,
+      pageId: null,
 
-      items: [
-        { id: 1, category: '카테고리 1', selected: false },
-        { id: 2, category: '카테고리 2', selected: false },
-        { id: 3, category: '카테고리 3', selected: false },
-      ],
+      isModalOpen: false,
+      isDragging: false,
+
       fileNames: [],
-      insertDTO: {
-        sj: null, //제목
-        cn: null, //내용
-        adres: null, // 주소
-        prdctnYear: null, // 생산연도
-        ty: 'P', // 타입 ( P: 사진, V: 영상 )
-        multipartFiles: null, // 첨부파일 정보
-        ctgryIds: null, // 카테고리 정보
+
+      // 등록/수정 요청 객체
+      reqDTO: {
+        dcryId: null,
+        sj: null,
+        cn: null,
+        adres: null,
+        prdctnYear: null,
+        ty: 'V',
+        fileId: null,
+        files: [],
+        ctgryIds: [],
       },
 
-      files: [],
+      multipartFiles: [],
       selectedCtgries: [], // 카테고리 목록
     };
   },
-  computed: {
-    filteredItems() {
-      // This could be modified to support filtering based on searchQuery
-      return this.items.filter(item =>
-        item.category.includes(this.searchQuery)
-      );
+
+  created() {
+    this.pageId = this.$route.query.id;
+    if (!this.$isEmpty(this.pageId)) {
+      this.fnFindDcry(); // 상세 조회
     }
   },
-  created() {
-  },
+
   methods: {
-    registerCategories() {
-      // Add selected categories to the displayed list
-      this.selectedCtgries = this.items
-        .filter(item => item.selected)
-        .map(item => item.category);
-      this.closeModal(); // Close modal after registration
-    },
-    removeCategory(index) {
-      // Remove category from the list
-      this.selectedCtgries.splice(index, 1);
-    },
-    searchCategories() {
-      // You can implement search logic if needed
-    },
-    nextPage() {
-      if (this.currentPage < this.totalPages) {
-        this.currentPage++;
-      }
-    },
-    previousPage() {
-      if (this.currentPage > 1) {
-        this.currentPage--;
-      }
-    },
-    showFileNames(event) {
-      const files = event.target.files;
-      this.fileNames = [];  // Clear previous file names
+    // 상세 조회
+    async fnFindDcry() {
+      try {
+        const response = await findDcryProc(this.pageId);
 
-      for (let i = 0; i < files.length; i++) {
-        const file = files[i];
-        const fileType = file.name.split('.').pop().toLowerCase();  // Get file extension
-
-        // Set default icon
-        let iconPath = this.fileicons;
-
-        // Determine the icon based on file type
-        if (['jpg', 'jpeg', 'png', 'gif'].includes(fileType)) {
-          iconPath = 'client/resources/images/icon/imgicon.png';  // Example for image files
-        } else if (['pdf'].includes(fileType)) {
-          iconPath = 'client/resources/images/icon/pdficon.png';  // Example for PDF files
-        } else if (['xls'].includes(fileType)) {
-          iconPath = 'client/resources/images/icon/excelicon.png';  // Example for audio files
-        } else if (['hwp'].includes(fileType)) {
-          iconPath = 'client/resources/images/icon/hwpicon.png';  // Example for video files
+        if (response.data.data.dcry.ty !== 'V') {
+          alert('올바른 접근이 아닙니다.');
+          this.fnMoveTo('VideoHistorySearch'); // 목록으로 이동
         }
 
-        // Push the file name and corresponding icon to the fileNames array
-        this.fileNames.push({
-          name: file.name,
-          icon: iconPath
-        });
+        this.copyToDcryReqDTO(response.data.data.dcry);
+      } catch (error) {
+        alert('조회중 오류가 발생했습니다.');
+        this.fnMoveTo('VideoHistorySearch'); // 목록으로 이동
+
+        if (error.response) {
+          alert(error.response.data.message);
+        }
+        console.error(error.message);
       }
     },
-    removeFile(index) {
-      // Remove file from the list
-      this.fileNames.splice(index, 1);
-      console.log(removeFile)
+
+    // dcry > reqDTO
+    copyToDcryReqDTO(dcry) {
+      const copyFields = Object.keys(this.reqDTO).filter(key => key !== 'dcryId' && key !== 'ty' && key !== 'files');
+      copyFields.forEach(field => {
+        this.reqDTO[field] = this.$isEmpty(dcry[field]) ? null : dcry[field];
+      });
+
+      this.reqDTO.ty = 'V'; // 영상기록물
+      this.reqDTO.files = dcry.files.length > 0 ? dcry.files : []; // 기존 첨부파일
+
+      this.multipartFiles = [];
+      this.selectedCtgries = dcry.ctgrys.length > 0 ? dcry.ctgrys : [];
+
+      console.log(this.reqDTO);
     },
-    openModal() {
-      this.isModalOpen = true;
+
+    // 카테고리 모달 열기/닫기
+    fnToggleModal() {
+      this.isModalOpen = !this.isModalOpen;
     },
-    // 모달 닫기
-    closeModal() {
-      this.isModalOpen = false;
+
+    // 카테고리 등록
+    fnAddCtgries(selectedCtgries) {
+      this.selectedCtgries = [...this.selectedCtgries, ...selectedCtgries];
+
+      this.fnToggleModal(); // 카테고리 모달 닫기
     },
+
+    // 카테고리 삭제
+    fnDelCtgry(id) {
+      this.selectedCtgries = this.selectedCtgries.filter(item => item.ctgryId !== id);
+    },
+
+    // 드래그 앤 드롭 이벤트 핸들러
+    handleDragOver(event) {
+      this.isDragging = true;
+    },
+    handleDragLeave(event) {
+      this.isDragging = false;
+    },
+    handleDrop(event) {
+      this.isDragging = false;
+
+      const files = event.dataTransfer.files;
+      if (files.length > 0) {
+        this.processFiles(files);
+      }
+    },
+    handleFileSelect(event) {
+      const files = event.target.files;
+      if (files.length > 0) {
+        this.processFiles(files);
+      }
+    },
+
+    // 파일 업로드 처리 함수
+    processFiles(files) {
+      const allowedTypes = ['mp4', 'mov', 'avi', 'wmv', 'mkv', 'webm']; // 영상 파일만 허용
+      const maxSize = 10 * 1024 * 1024 * 1024; // 10GB
+
+      // 유효성 검사
+      if (files.length > 1 + this.multipartFiles.length + this.reqDTO.files.length > 0) {
+        alert("영상 파일은 한 개만 등록 가능합니다.");
+        return;
+      }
+
+      for (let file of files) {
+        const fileType = file.name.split('.').pop().toLowerCase();
+
+        // 파일 타입 검증
+        if (!allowedTypes.includes(fileType)) {
+          alert(`${file.name} 파일은 허용되지 않는 형식입니다. 이미지 파일(jpg, jpeg, png, gif)만 업로드 가능합니다.`);
+          return;
+        }
+
+        // 파일 크기 제한 검증
+        if (file.size > maxSize) {
+          alert(`${file.name} 파일이 10GB를 초과합니다.`);
+          return;
+        }
+
+        this.multipartFiles.push(file);
+      }
+    },
+
+    // 파일 삭제
+    fnDelFile(type, separator) {
+      if (type === 'new') {
+        this.multipartFiles.splice(separator, 1);
+      } else if (type === 'old') {
+        this.reqDTO.files = this.reqDTO.files.filter(item => item.fileId !== separator);
+      }
+    },
+
+    // 등록
+    async submitForm() {
+      // 유효성 검사
+      if (!this.reqDTO.sj) {
+        alert("제목을 입력해 주세요.");
+        return;
+      }
+      if (this.$isEmpty(this.pageId) && this.multipartFiles.length == 0) {
+        alert("파일을 1개 이상 첨부해 주세요.");
+        return;
+      }
+
+      try {
+        const formData = new FormData();
+
+        // 텍스트 데이터 추가
+        formData.append('sj', this.reqDTO.sj);
+        formData.append('cn', this.reqDTO.cn);
+        formData.append('adres', this.reqDTO.adres);
+        formData.append('prdctnYear', this.reqDTO.prdctnYear);
+        formData.append('ty', this.reqDTO.ty);
+
+        // 게시물 아이디
+        if (!this.$isEmpty(this.pageId)) {
+          formData.append('dcryId', this.pageId);
+        }
+
+        // 파일 아이디
+        if (!this.$isEmpty(this.reqDTO.fileId)) {
+          formData.append('fileId', this.reqDTO.fileId);
+        }
+
+        // 카테고리 Ids 추가
+        if (this.selectedCtgries && this.selectedCtgries.length > 0) {
+          for (let ctgry of this.selectedCtgries) {
+            formData.append("ctgryIds", ctgry.ctgryId);
+          }
+        }
+
+        // 파일 추가
+        for (let file of this.multipartFiles) {
+          formData.append("multipartFiles", file);
+        }
+
+        // 기존파일 수정
+        if (!this.$isEmpty(this.pageId) && this.reqDTO.files.length > 0) {
+          for (let file of this.reqDTO.files) {
+            formData.append("files", file.fileId);
+          }
+        }
+
+        // API 통신
+        const response = this.$isEmpty(this.pageId) ? await saveDcry(formData) : await updateDcry(formData);
+        let result = response.data;
+        let id = result.data.dcryId;
+        alert(this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다.");
+
+        // 상세 페이지로 이동
+        this.fnMoveTo('VideoHistoryDetail', id);
+      } catch (error) {
+        if (error.response) {
+          alert(error.response.data.message);
+        } else {
+          alert("에러가 발생했습니다.");
+        }
+        console.error(error.message);
+      };
+    },
+
+    // 페이지 이동
+    fnMoveTo(page, id) {
+      if (this.$isEmpty(id)) {
+        this.$router.push({ name: page });
+      } else {
+        this.$router.push({ name: page, query: { id: id } });
+      }
+    }
   }
 };
 </script>
+<style>
+.file-label {
+  border: 2px dashed #ddd;
+  border-radius: 8px;
+  padding: 20px;
+  text-align: center;
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+
+.file-label:hover {
+  border-color: #aaa;
+  background-color: #f9f9f9;
+}
+
+.file-label.drag-over {
+  border-color: #1890ff;
+  background-color: rgba(24, 144, 255, 0.1);
+}
+</style>
(파일 끝에 줄바꿈 문자 없음)
client/views/pages/user/VideoHistorySearch.vue
--- client/views/pages/user/VideoHistorySearch.vue
+++ client/views/pages/user/VideoHistorySearch.vue
@@ -4,8 +4,8 @@
       <h2>영상 기록물</h2>
       <div class="breadcrumb-list">
         <ul>
-          <!-- Bind the image source dynamically for homeicon -->
-          <li><img :src="homeicon" alt="Home Icon">
+          <li>
+            <img :src="homeicon" alt="Home Icon">
             <p>기록물</p>
           </li>
           <li><img :src="righticon" alt=""></li>
@@ -13,24 +13,32 @@
         </ul>
       </div>
     </div>
-    <div action="search" class="search-form form ">
+    <div class="search-form form">
       <dl>
         <dd class="mb-15">
           <p>검색범위</p>
           <ul>
             <li>
-              <input type="checkbox" id="allScope" v-model="isChkAllScope" @change="fnChkAllOptions('scope')" />
+              <input type="checkbox" id="allScope" v-model="isChkAllScope" @change="fnChkAllOptions" />
               <label for="allScope">전체</label>
             </li>
-            <li v-for="(scope, idx) in searchType" :key="idx">
-              <input type="checkbox" :id="idx" :name="searchType" :value="scope.key" v-model="searchReqDTO.searchType" @change="fnChkOption('scope')" />
-              <label :for="idx">{{ scope.value }}</label>
+            <li>
+              <input type="checkbox" id="searchSj" v-model="searchReqDTO.useSj" @change="fnChkOption" />
+              <label for="searchSj">제목</label>
+            </li>
+            <li>
+              <input type="checkbox" id="searchCn" v-model="searchReqDTO.useCn" @change="fnChkOption" />
+              <label for="searchCn">내용</label>
+            </li>
+            <li>
+              <input type="checkbox" id="searchAdres" v-model="searchReqDTO.useAdres" @change="fnChkOption" />
+              <label for="searchAdres">주소</label>
             </li>
           </ul>
         </dd>
         <dd class="mb-15">
           <p>검색어</p>
-          <div class="wfull"><input type="text" v-model="searchReqDTO.searchText"></div>
+          <div class="wfull"><input type="text" v-model="searchReqDTO.searchText" v-on:keyup.enter="fnSearch()"></div>
         </dd>
         <dd class="mb-15">
           <p>생산연도</p>
@@ -42,8 +50,8 @@
           <p>카테고리</p>
           <ul>
             <li v-for="(category, idx) of categorys" :key="idx">
-              <input type="checkbox" :id="category.ctgryId" name="categorys" :value="category.ctgryId" v-model="searchReqDTO.searchCtgry" />
-              <label :for="category.ctgryId">{{ category.ctgryNm }}</label>
+              <input type="checkbox" :id="'ctgry_' + idx" name="categorys" :value="category.ctgryId" v-model="searchReqDTO.searchCtgries" />
+              <label :for="'ctgry_' + idx">{{ category.ctgryNm }}</label>
             </li>
           </ul>
         </dd>
@@ -57,10 +65,12 @@
           </ul>
         </dd>
         <div class="btn-group">
-          <button class="reset"><img :src="reseticon" alt="">
+          <button type="button" class="reset" @click="init">
+            <img :src="reseticon" alt="">
             <p>초기화</p>
           </button>
-          <button class="search"><img :src="searchicon" alt="">
+          <button type="button" class="search" @click="fnSearch">
+            <img :src="searchicon" alt="">
             <p>검색</p>
           </button>
         </div>
@@ -71,213 +81,66 @@
         <div class="flex-sp-bw mb-20 align-center">
           <div class="resultext ">
             <img :src="resulticon" alt="">
-            <p>총 <b>{{ count }}개</b>의 영상 기록물이 검색되었습니다. </p>
+            <p>총 <b>{{ searchReqDTO.totalRecordCount }}개</b>의 영상 기록물이 검색되었습니다. </p>
           </div>
-          <div class="flex ">
+          <div class="flex">
             <ul class="tab-box mb-20">
-              <li v-for="(tab, index) in tabs" :key="index" class="tab-title" :class="{ active: selectedTab === tab.id }" @click="selectTab(tab.id)">
-                <img :src="selectedTab === tab.id ? tab.activeImage : tab.inactiveImage" :alt="tab.title" class="tab-icon" />
+              <li v-for="(tab, idx) in tabs" :key="idx" class="tab-title" :class="{ active: selectedTabId === tab.id }" @click="selectTab(tab.id)">
+                <img :src="selectedTabId === tab.id ? tab.activeImage : tab.inactiveImage" :alt="tab.title" class="tab-icon" />
                 <p><b>{{ tab.title }}</b></p>
               </li>
             </ul>
             <div class="select-box">
-              <select v-model="itemsPerPage" @change="changeItemsPerPage">
-                <option :value="5" selected>5개</option>
-                <option :value="10">10개</option>
-                <option :value="15">15개</option>
+              <select v-model="searchReqDTO.recordSize" @change="fnSearch">
+                <option :value="24">24개</option>
+                <option :value="36">36개</option>
+                <option :value="100">100개</option>
               </select>
             </div>
           </div>
         </div>
         <div class="tab-content">
-          <!-- Loop through tabContents, and only display content that matches selectedTab -->
-          <div v-for="(tabContent, idx) in tabContents" :key="idx">
-            <!-- Display content only if the tab's ID matches the selectedTab -->
-            <div v-show="tabContent.id === selectedTab">
-              <!-- 카드형 Section (Card Layout) -->
-              <div v-if="tabContent.viewType === 'card'">
-                <ul class="card-wrap">
-                  <li v-for="(resultitem, index) in paginatedItems" :key="index" class="mb-30">
-                    <div class="result-box">
-                      <!-- Main Image Section -->
-                      <div class="main-img">
-                        <img :src="resultitem.img" alt="" class="tab-image" />
-                      </div>
-                      <!-- Text Section -->
-                      <div class="text-box">
-                        <router-link :to="{ path: '/VideoHistoryDetail.page' }">
-                          <h5>{{ resultitem.title }}</h5>
-                        </router-link>
-                        <p class="address">{{ resultitem.address }}</p>
-                        <p class="text">{{ resultitem.content }}</p>
-                        <div class="mb-20">
-                          <ul class="category">
-                            <li v-if="resultitem.category1" class="category1">카테고리1</li>
-                            <li v-if="resultitem.category2" class="category2">카테고리2</li>
-                          </ul>
-                        </div>
-                        <div class="date">
-                          <ul>
-                            <li>생산연도 <b>{{ resultitem.year }}</b></li>
-                            <li>|</li>
-                            <li>등록 <b>{{ resultitem.date }}</b></li>
-                          </ul>
-                        </div>
-                      </div>
-                    </div>
-                  </li>
-                </ul>
-                <!-- Empty State if no results in paginatedItems -->
-                <div v-if="paginatedItems.length === 0" class="no-results">
-                  <p>등록된 게시물이 없습니다.</p>
-                </div>
-              </div>
-              <!-- 리스트형 Section (List Layout) -->
-              <div v-if="tabContent.viewType === 'list'">
-                <ul class="list-wrap">
-                  <li v-for="(resultitem, index) in paginatedItems" :key="index" class="mb-30">
-                    <div class="text-box">
-                      <router-link :to="{ path: '/VideoHistoryDetail.page' }">
-                        <h5>{{ resultitem.title }}</h5>
-                      </router-link>
-                      <p class="address">{{ resultitem.address }}</p>
-                      <div class="flex-sp-bw">
-                        <div class="mb-20">
-                          <ul class="category">
-                            <li v-if="resultitem.category1" class="category1">카테고리1</li>
-                            <li v-if="resultitem.category2" class="category2">카테고리2</li>
-                          </ul>
-                        </div>
-                        <div class="date ">
-                          <ul>
-                            <li>생산연도 <b>{{ resultitem.year }}</b></li>
-                            <li>|</li>
-                            <li>등록 <b>{{ resultitem.date }}</b></li>
-                          </ul>
-                        </div>
-                      </div>
-                    </div>
-                  </li>
-                </ul>
-                <!-- Empty State if no results in paginatedItems -->
-                <div v-if="paginatedItems.length === 0" class="no-results">
-                  <p>등록된 게시물이 없습니다.</p>
-                </div>
-              </div>
-            </div>
+          <div v-if="searchResult.length > 0">
+            <CardStyleComponent v-if="selectedTabId === 'CARD'" :name="'V'" :list="searchResult" />
+            <ListStyleComponent v-if="selectedTabId === 'LIST'" :name="'V'" :list="searchResult" />
+          </div>
+          <div v-else class="no-results">
+            <p>등록된 게시물이 없습니다.</p>
           </div>
         </div>
       </div>
       <div class="btn-group flex-end mt-40"><button class="register"> <router-link :to="{ path: '/VideoHistoryInsert.page' }">등록</router-link></button></div>
-      <div class="pagination flex-center mt-40">
-        <!-- Previous and Next Page Buttons -->
-        <button>
-          <DoubleLeftOutlined />
-        </button>
-        <button @click="previousPage" :disabled="currentPage === 1">
-          <LeftOutlined />
-        </button>
-        <button class="page-number clicked">1</button>
-        <button @click="nextPage" :disabled="currentPage === totalPages">
-          <RightOutlined />
-        </button>
-        <button>
-          <DoubleRightOutlined />
-        </button>
-      </div>
+      <DefaultPagination class="mt-40" :search="searchReqDTO" @onChange="fnChangeCurrentPage" />
     </div>
   </div>
 </template>
 <script>
-import { DoubleLeftOutlined, LeftOutlined, RightOutlined, DoubleRightOutlined } from '@ant-design/icons-vue';
-import { findAllCategoryProc } from "../../../resources/api/category"; // 카테고리 목록 검색
+// COMPONENT
+import CardStyleComponent from '@/views/component/listLayout/CardStyleComponent.vue';
+import ListStyleComponent from '@/views/component/listLayout/ListStyleComponent.vue';
+import DefaultPagination from '@/views/component/DefaultPagination.vue';
+// API
+import { findAllByNullProc } from "@/resources/api/category";
+import { findDcrysProc } from "@/resources/api/dcry";
 
 export default {
   components: {
-    DoubleLeftOutlined,
-    LeftOutlined,
-    RightOutlined,
-    DoubleRightOutlined,
+    DefaultPagination,
+    CardStyleComponent,
+    ListStyleComponent,
   },
+
   data() {
     return {
-      selectedTab: 1,
-      // 검색용 객체
-      searchReqDTO: {
-        searchType: [],
-        searchText: null,
-        startYear: null,
-        endYear: null,
-        searchTy: null,
-        searchCtgry: [],
-        order: "rgsde",
-      },
-      tabs: [
-
-        {
-          id: 1,
-          title: "카드형",
-          activeImage: "client/resources/images/list_icon01_on.png", // Active tab image
-          inactiveImage: "client/resources/images/list_icon01_off.png",
-        },
-        {
-          id: 2,
-          title: "리스트형",
-          activeImage: "client/resources/images/list_icon02_on.png", // Active tab image
-          inactiveImage: "client/resources/images/list_icon02_off.png",
-        },
-      ],
-      tabContents: [
-        { id: 1, viewType: 'card', list: [{ sj: 'Item 1', rgsde: '2025-03-01', files: [{ filePath: 'image1.png' }] }] },
-        { id: 2, viewType: 'list', list: [{ sj: 'Item 2', rgsde: '2025-03-02', files: [{ filePath: 'image2.png' }] }] },
-      ],
-      paginatedItems: [],
-      resultitems: [
-        {
-          img: 'client/resources/images/img6.png',
-          title: '영상 기록물 제목',
-          address: '경상북도 구미시 송정대로 55',
-          content: '대한민국 최대의 내륙 산업단지를 보유하고, 서울로부터 277km, 부산으로부터 167km 거리에 있으며, 면적은 615㎢로 경상북도 전체 면적의 3.2%에 달합니다. 인구는 41만 명이고, 선산읍, 고아읍, 산동읍을 비롯한 3읍, 5면, 17개 동으로 구성되어…',
-          category1: true,
-          category2: true,
-          year: 2020,
-          date: '2021-01-01'
-        },
-
-      ],
-      currentPage: 1, // Current page number
-      itemsPerPage: 5,
+      // ICON
       resulticon: "client/resources/images/icon/r-check.png",
       homeicon: 'client/resources/images/icon/home.png',
       searchicon: 'client/resources/images/icon/search.png',
       reseticon: 'client/resources/images/icon/reset.png',
       righticon: 'client/resources/images/icon/right.png',
-      count: 23,
-      checkOptions: [
-        '전체',
-        '사진',
-        '영상',
-        '미디어 영상',
-        '보도자료',
-      ],
-      checkOptions2: [
-        '전체',
-        '제목',
-        '내용',
-        '주소',
-      ],
-      checkOptions3: [
-        '카테고리1',
-        '카테고리2',
-        '카테고리3',
-        '카테고리4',
-        '카테고리5',
-      ],
-      checkOptions4: [
-        '최신',
-        '인기',
-      ],
-      isChkAllScope: false, // 검색범위 전체 체크 여부
+
+      // 검색용 객체
+      isChkAllScope: true, // 검색범위 전체 체크 여부
       searchType: [
         { key: "sj", value: "제목" },
         { key: "cn", value: "내용" },
@@ -288,105 +151,144 @@
         { key: "rgsde", value: "최신" },
         { key: "rdcnt", value: "인기" },
       ], // 정렬 목록
+
+      // 검색용 객체 초기값
+      searchDefault: {
+        useSj: true,
+        useCn: true,
+        useAdres: true,
+        searchText: null,
+        startYear: null,
+        endYear: null,
+        searchTy: "V",
+        searchCtgries: [],
+        order: "rgsde",
+        // 페이지네이션
+        currentPage: 1, // 현재 페이지
+        recordSize: 24, // 한 페이지에 표시할 데이터 개수
+      },
+      searchReqDTO: {}, // 실제 검색에 사용되는 객체
+
+      searchResult: [], // 검색결과
+
+      // 목록 레이아웃
+      selectedTabId: null,
+      tabs: [
+        {
+          id: "CARD",
+          title: "카드형",
+          activeImage: "client/resources/images/list_icon01_on.png", // Active tab image
+          inactiveImage: "client/resources/images/list_icon01_off.png",
+        },
+        {
+          id: "LIST",
+          title: "리스트형",
+          activeImage: "client/resources/images/list_icon02_on.png", // Active tab image
+          inactiveImage: "client/resources/images/list_icon02_off.png",
+        },
+      ],
+
+      isInitialLoad: true // 초기 로드 여부
     };
   },
-  computed: {
-    // Total number of pages
-    totalPages() {
-      return Math.ceil(this.resultitems.length / this.itemsPerPage);
-    },
 
-    // Paginated items based on current page and items per page
-    paginatedItems() {
-      const start = (this.currentPage - 1) * this.itemsPerPage;
-      const end = start + this.itemsPerPage;
-      return this.resultitems.slice(start, end);
-    },
-  },
   created() {
-    // 초기 데이터 세팅
-    this.isChkAllScope = true;
-    this.searchReqDTO.searchType = this.searchType.map(item => item.key);
-    this.searchReqDTO.order = this.orders[0].key
-
+    this.init(); // 초기화
     this.fnFindCategorys(); // 카테고리 목록 조회 (검색조건 없음)
   },
+
+  mounted() {
+    let searchText = this.$route.query.searchText;
+    if (searchText !== null) {
+      this.searchReqDTO.searchText = searchText;
+    }
+
+    this.fnSearch(); // 통합검색
+  },
+
   methods: {
-    selectTab(tabId) {
-      this.selectedTab = tabId; // Update the selected tab index
-    },
-    // Change the number of items displayed per page
-    changeItemsPerPage() {
-      this.currentPage = 1; // Reset to first page when changing items per page
-    },
-    previousPage() {
-      if (this.currentPage > 1) {
-        this.currentPage--;
+    // 초기화
+    init() {
+      if (this.isInitialLoad) {
+        this.isInitialLoad = false;
+      } else {
+        if (!confirm('검색 조건을 초기화하시겠습니까?')) {
+          return;
+        }
       }
+
+      this.searchReqDTO = JSON.parse(JSON.stringify(this.searchDefault));
+      this.searchResult = []; // 검색결과 초기화
+
+      this.selectedTabId = this.tabs[0].id;
+
+      this.fnSearch(); // 통합검색
     },
 
-    // Go to the next page
-    nextPage() {
-      if (this.currentPage < this.totalPages) {
-        this.currentPage++;
-      }
-    },
-    isChkAllScope: false, // 검색범위 전체 체크 여부
-    searchType: [
-      { key: "sj", value: "제목" },
-      { key: "cn", value: "내용" },
-      { key: "adres", value: "주소" },
-    ], // 검색범위 목록
-    categorys: [], // 카테고리 목록
-    orders: [
-      { key: "rgsde", value: "최신" },
-      { key: "rdcnt", value: "인기" },
-    ], // 정렬 목록
-
+    // 카테고리 목록 조회
     async fnFindCategorys() {
       try {
-        const response = await findAllCategoryProc();
+        const response = await findAllByNullProc();
         this.categorys = response.data.data.ctgry;
       } catch (error) {
+        this.categorys = []; // 카테고리 목록 초기화
+
         if (error.response) {
-          console.log("에러 응답:", error.response.data);
+          alert(error.response.data.message);
         }
-        console.error("Error:", error);
+        console.error(error.message);
       }
+    },
+
+    // 페이지 이동
+    fnChangeCurrentPage(currentPage) {
+      this.searchReqDTO.currentPage = Number(currentPage);
+      this.$nextTick(() => {
+        this.fnSearch();
+      });
     },
 
     // 통합검색
-    async fnFindAllDatas() {
+    async fnSearch() {
       try {
-        let params = {};
-        if (this.searchReqDTO.searchRecord.length > 0) {
-          params.searchRecords = this.searchReqDTO.searchRecord.join(',');
+        const params = JSON.parse(JSON.stringify(this.searchReqDTO));
+
+        // 카테고리 목록 처리
+        if (this.searchReqDTO.searchCtgries && this.searchReqDTO.searchCtgries.length > 0) {
+          params.searchCtgries = this.searchReqDTO.searchCtgries.join(',');
+        } else {
+          delete params.searchCtgries;
         }
-        if (this.searchReqDTO.searchType.length > 0) {
-          params.searchTypes = this.searchReqDTO.searchType.join(',');
-        }
-        if (this.searchReqDTO.searchCtgry.length > 0) {
-          params.searchCtgries = this.searchReqDTO.searchCtgry.join(',');
-        }
-        params.searchText = this.searchReqDTO.searchText;
-        params.startYear = this.searchReqDTO.startYear;
-        params.endYear = this.searchReqDTO.endYear;
-        params.order = this.searchReqDTO.order;
 
         // API 호출
-        const response = await findAllDatas(params);
-        this.searchResult = response.data.data.searchResult;
+        const response = await findDcrysProc(params);
+        this.searchResult = response.data.data.dcrys;
+        this.searchReqDTO = response.data.data.search;
       } catch (error) {
+        this.searchResult = []; // 검색결과 초기화
+
         if (error.response) {
-          console.log("에러 응답:", error.response.data);
+          alert(error.response.data.message);
         }
-        console.error("Error:", error);
+        console.error(error.message);
       }
     },
+
+    // 기록유형 전체 선택 여부 변경
+    fnChkAllOptions() {
+      this.searchReqDTO.useSj = this.isChkAllScope;
+      this.searchReqDTO.useCn = this.isChkAllScope;
+      this.searchReqDTO.useAdres = this.isChkAllScope;
+    },
+
+    // 기록유형 선택 여부 변경
+    fnChkOption() {
+      this.isChkAllScope = this.searchReqDTO.useSj && this.searchReqDTO.useCn && this.searchReqDTO.useAdres;
+    },
+
+    selectTab(tabId) {
+      this.selectedTabId = tabId;
+    },
   },
-
-
-
 };
-</script>
-<style scoped></style>
(파일 끝에 줄바꿈 문자 없음)
+</script>
(파일 끝에 줄바꿈 문자 없음)
package.json
--- package.json
+++ package.json
@@ -18,7 +18,6 @@
     "vue": "3.5.13",
     "vue-loader": "^17.2.2",
     "vue-router": "^4.3.2",
-    "vue3-youtube": "^0.1.9",
     "vuex": "^4.1.0",
     "vuex-persistedstate": "^4.1.0",
     "webpack": "5.74.0",
Add a comment
List