jichoi / calendar star
박정하 박정하 3 days ago
250808 박정하 휴가 1차 수정
@6a85159f3b8ce9d122bcf459a78e8f0bf247f0a7
client/resources/api/vcatn.js
--- client/resources/api/vcatn.js
+++ client/resources/api/vcatn.js
@@ -1,36 +1,46 @@
-import {apiClient} from "./index";
+import { apiClient } from "./index";
 
-// 등록
+// 휴가 등록
 export const saveVcatnProc = data => {
   return apiClient.post('/vcatn/saveVcatn.json', data);
 }
 
-// 조회 - 현황
-export const findVcatnsSummary = () => {
-  return apiClient.get('/vcatn/findVcatnsSummary.json');
+// 휴가 현황 조회
+export const findVcatnSummaryProc = () => {
+  return apiClient.get('/vcatn/findVcatnSummary.json');
 }
 
-// 조회 - 목록
+// 휴가 목록 조회
 export const findVcatnsProc = data => {
   return apiClient.get('/vcatn/findVcatns.json', { params: data });
 }
 
-// 조회 - 상세
+// 휴가 상세 조회
 export const findVcatnProc = data => {
   return apiClient.get(`/vcatn/${data}/findVcatn.json`);
 }
 
-// 조회 - 이전 승인자
-export const findLastVcatnProc = () => {
-  return apiClient.get('/vcatn/findLastVcatn.json');
+// 휴가 수정
+export const updateVcatnProc = (id, data) => {
+  return apiClient.put(`/vcatn/${id}/updateVcatn.json`, data);
 }
 
-// 삭제
+// 휴가 삭제
 export const deleteVcatnProc = data => {
   return apiClient.delete(`/vcatn/${data}/deleteVcatn.json`);
 }
 
-// 수정
-export const updateVcatnProc = data => {
-  return apiClient.post('/vcatn/updateVcatn.json', data);
+// 휴가 이전 승인자 조회
+export const findLastVcatnProc = () => {
+  return apiClient.get('/vcatn/findLastVcatn.json');
+}
+
+// 휴가 공통코드 조회
+export const findVcatnKndsProc = () => {
+  return apiClient.get('/vcatn/findVcatnKnds.json');
+}
+
+// 휴가 재신청
+export const reapplyVcatnProc = (id, data) => {
+  return apiClient.post(`/vcatn/${id}/reapplyVcatn.json`, data);
 }
(파일 끝에 줄바꿈 문자 없음)
client/resources/js/cmmnPlugin.js
--- client/resources/js/cmmnPlugin.js
+++ client/resources/js/cmmnPlugin.js
@@ -116,46 +116,5 @@
 
       return lists;
     };
-
-    // 기본 코드 목록 초기화
-    Vue.config.globalProperties.$defaultCodes = async () => {
-      const codeGroups = {
-        vcatnKndCodeList: [], // 휴가 종류 (연차/반차)
-        bsrpCodeList: [],     // 출장 종류 (해외/국내)
-        sanctnCodeList: [],   // 결재 구분 (결재/대결/전결)
-        confmCodeList: [],    // 상태 코드 (대기/결재대기/승인/반려)
-        clsfCodeList: [],     // 직급 코드 (사원/주임/대리)
-        rspofcCodeList: [],   // 직책 코드 (팀장)
-      };
-
-      // 휴가 종류 - depth 2 조회
-      const vcatnKndCodes = await Vue.config.globalProperties.$findChildCodes('sanctn_mby_vcatn');
-      for (const code of vcatnKndCodes) {
-        const childCodes = await Vue.config.globalProperties.$findChildCodes(code.code);
-        codeGroups.vcatnKndCodeList.push(...childCodes);
-      }
-
-      // 출장 종류 - depth 1 조회
-      const bsrpCodes = await Vue.config.globalProperties.$findChildCodes('sanctn_mby_bsrp');
-      codeGroups.bsrpCodeList.push(...bsrpCodes);
-
-      // 결재 구분 - depth 1 조회
-      const sanctnCodes = await Vue.config.globalProperties.$findChildCodes('sanctn_code');
-      codeGroups.sanctnCodeList.push(...sanctnCodes);
-
-      // 상태 코드 - depth 1 조회
-      const confmCodes = await Vue.config.globalProperties.$findChildCodes('confm_code');
-      codeGroups.confmCodeList.push(...confmCodes);
-
-      // 직급 코드 - depth 1 조회
-      const clsfCodes = await Vue.config.globalProperties.$findChildCodes('clsf_code');
-      codeGroups.clsfCodeList.push(...clsfCodes);
-
-      // 직책 코드 - depth 1 조회
-      const rspofcCodes = await Vue.config.globalProperties.$findChildCodes('rspofc_code');
-      codeGroups.rspofcCodeList.push(...rspofcCodes);
-
-      return codeGroups;
-    };
   },
 }
(파일 끝에 줄바꿈 문자 없음)
client/views/component/Sanctn/SanctnFormList.vue
--- client/views/component/Sanctn/SanctnFormList.vue
+++ client/views/component/Sanctn/SanctnFormList.vue
@@ -1,29 +1,30 @@
 <template>
   <div>
     <div v-for="(lists, idx) of lists" :key="lists.sanctnId || idx" class="draggable-item-wrapper">
-      <div class="drop-zone" @dragover.prevent="handleDragEnter($event, idx)" @dragenter.prevent="handleDragEnter($event, idx)" @dragleave="handleDragLeave($event, idx)" @drop.prevent="handleDrop($event, idx)" :class="{
-        'drop-active': dropTarget === idx,
-        'drop-visible': draggedIndex !== null && shouldShowDropZone(idx),
-        'drop-hidden': draggedIndex !== null && !shouldShowDropZone(idx)
+      <div class="drop-zone" @dragover.prevent="handleDragOver($event, idx)" @dragenter.prevent="handleDragEnter($event, idx)" @dragleave="handleDragLeave($event, idx)" @drop.prevent="handleDrop($event, idx)" :class="{
+        'drop-active': currentDropTarget === idx,
+        'drop-visible': isDragging && shouldShowDropZone(idx),
+        'drop-hidden': isDragging && !shouldShowDropZone(idx)
       }">
         <div class="drop-indicator">여기에 놓기</div>
       </div>
-      <div class="d-flex addapproval draggable-item" draggable="true" @dragstart="handleDragStart(idx, $event)" @dragend="handleDragEnd" :class="{ 'being-dragged': draggedIndex === idx }">
+      <div class="d-flex addapproval draggable-item" draggable="true" @dragstart="handleDragStart(idx, $event)" @dragend="handleDragEnd" :class="{ 'being-dragged': currentDraggedIndex === idx }">
         <select class="form-select" v-model="lists.sanctnSe" style="width: 110px;" @mousedown.stop>
-          <option v-for="(item, idx) of cmmnCodes.sanctnCodeList" :key="idx" :value="item.code"> {{ item.codeNm }} </option>
+          <option v-for="(item, idx) of approvalCodes" :key="idx" :value="item.code"> {{ item.codeNm }} </option>
         </select>
         <div class="d-flex align-items-center border-x">
           <p>{{ lists.confmerNm }} {{ lists.clsfNm }} ({{ lists.sanctnOrdr }})</p>
-          <button type="button" @click="$emit('delSanctn', idx)" @mousedown.stop>
+          <button type="button" @click="removeApproval(idx)" @mousedown.stop>
             <CloseOutlined />
           </button>
         </div>
       </div>
     </div>
-    <div class="drop-zone" @dragover.prevent="handleDragEnter($event, lists.length)" @dragenter.prevent="handleDragEnter($event, lists.length)" @dragleave="handleDragLeave($event, lists.length)" @drop.prevent="handleDrop($event, lists.length)" :class="{
-      'drop-active': dropTarget === lists.length,
-      'drop-visible': draggedIndex !== null && shouldShowLastDropZone(),
-      'drop-hidden': draggedIndex !== null && !shouldShowLastDropZone()
+    <!-- 마지막 드롭존 -->
+    <div class="drop-zone" @dragover.prevent="handleDragOver($event, lists.length)" @dragenter.prevent="handleDragEnter($event, lists.length)" @dragleave="handleDragLeave($event, lists.length)" @drop.prevent="handleDrop($event, lists.length)" :class="{
+      'drop-active': currentDropTarget === lists.length,
+      'drop-visible': isDragging && shouldShowLastDropZone(),
+      'drop-hidden': isDragging && !shouldShowLastDropZone()
     }">
       <div class="drop-indicator">여기에 놓기</div>
     </div>
@@ -35,7 +36,9 @@
 export default {
   name: 'SanctnList',
 
-  components: { CloseOutlined },
+  components: {
+    CloseOutlined
+  },
 
   props: {
     lists: {
@@ -44,80 +47,140 @@
     }
   },
 
+  emits: ['update:lists', 'delSanctn'],
+
   data() {
     return {
-      cmmnCodes: {}, // 결재 코드 목록
-      draggedIndex: null,
-      dropTarget: null,
+      approvalCodes: [], // 결재 코드 목록
+      currentDraggedIndex: null,
+      currentDropTarget: null,
     }
+  },
+
+  computed: {
+    // 드래그 중인지 여부
+    isDragging() {
+      return this.currentDraggedIndex !== null;
+    },
   },
 
   async created() {
-    this.cmmnCodes = await this.$defaultCodes(); // 코드 목록 초기화
+    this.loadApprovalCodes(); // 코드 목록 초기화
   },
 
   methods: {
-    shouldShowDropZone(index) {
-      if (this.draggedIndex === null) return true;
-      if (index === this.draggedIndex || index === this.draggedIndex + 1) return false;
-      return true;
+    // 결재 코드 목록 로드
+    async loadApprovalCodes() {
+      try {
+        this.approvalCodes = await this.$findChildCodes('sanctn_code');
+      } catch (error) {
+        console.error('결재 코드 로드 실패:', error);
+        this.approvalCodes = [];
+      }
     },
 
+    // 드롭존 표시 여부 계산
+    shouldShowDropZone(targetIndex) {
+      if (!this.isDragging) return true;
+
+      // 현재 드래그 중인 아이템의 앞뒤 드롭존은 숨김
+      const adjacentIndexes = [this.currentDraggedIndex, this.currentDraggedIndex + 1];
+      return !adjacentIndexes.includes(targetIndex);
+    },
+
+    // 마지막 드롭존 표시 여부 계산
     shouldShowLastDropZone() {
-      if (this.draggedIndex === null) return true;
-      return this.draggedIndex !== this.lists.length - 1;
+      if (!this.isDragging) return true;
+
+      // 마지막 아이템을 드래그 중이면 마지막 드롭존 숨김
+      return this.currentDraggedIndex !== this.lists.length - 1;
     },
 
-    handleDragStart(index, event) {
-      this.draggedIndex = index;
+    // 드래그 시작
+    handleDragStart(itemIndex, event) {
+      this.currentDraggedIndex = itemIndex;
       event.dataTransfer.effectAllowed = 'move';
-      event.dataTransfer.setData('text/plain', index.toString());
+      event.dataTransfer.setData('text/plain', itemIndex.toString());
     },
 
+    // 드래그 오버 (드롭 허용)
+    handleDragOver(event, dropIndex) {
+      event.preventDefault();
+      event.dataTransfer.dropEffect = 'move';
+    },
+
+    // 드래그 진입
     handleDragEnter(event, dropIndex) {
-      if (this.draggedIndex !== null) {
-        this.dropTarget = dropIndex;
+      if (this.isDragging) {
+        this.currentDropTarget = dropIndex;
       }
     },
 
+    // 드래그 떠남
     handleDragLeave(event, dropIndex) {
-      const rect = event.currentTarget.getBoundingClientRect();
-      const x = event.clientX;
-      const y = event.clientY;
+      const dropZone = event.currentTarget;
+      const rect = dropZone.getBoundingClientRect();
+      const { clientX: x, clientY: y } = event;
 
-      if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
-        if (this.dropTarget === dropIndex) {
-          this.dropTarget = null;
-        }
+      // 드롭존 영역을 완전히 벗어났는지 확인
+      const isOutside = x < rect.left || x > rect.right || y < rect.top || y > rect.bottom;
+
+      if (isOutside && this.currentDropTarget === dropIndex) {
+        this.currentDropTarget = null;
       }
     },
 
+    // 드롭 처리
     handleDrop(event, dropIndex) {
-      if (this.draggedIndex !== null && this.draggedIndex !== dropIndex) {
-        let finalDropIndex = dropIndex;
-
-        if (this.draggedIndex < dropIndex) {
-          finalDropIndex = dropIndex - 1;
-        }
-
-        const newSanctns = [...this.lists];
-        const draggedItem = newSanctns.splice(this.draggedIndex, 1)[0];
-        newSanctns.splice(finalDropIndex, 0, draggedItem);
-
-        newSanctns.forEach((item, index) => {
-          item.sanctnOrdr = index + 1;
-        });
-
-        this.$emit('update:lists', newSanctns);
+      if (!this.isDragging || this.currentDraggedIndex === dropIndex) {
+        this.resetDragState();
+        return;
       }
 
-      this.dropTarget = null;
+      this.reorderApprovalList(dropIndex);
+      this.resetDragState();
     },
 
+    // 드래그 종료
     handleDragEnd() {
-      this.draggedIndex = null;
-      this.dropTarget = null;
-    }
+      this.resetDragState();
+    },
+
+    // 결재 목록 재정렬
+    reorderApprovalList(dropIndex) {
+      let finalDropIndex = dropIndex;
+
+      // 드래그한 아이템이 드롭 위치보다 앞에 있으면 인덱스 조정
+      if (this.currentDraggedIndex < dropIndex) {
+        finalDropIndex = dropIndex - 1;
+      }
+
+      const reorderedList = [...this.lists];
+      const [draggedItem] = reorderedList.splice(this.currentDraggedIndex, 1);
+      reorderedList.splice(finalDropIndex, 0, draggedItem);
+
+      // 결재 순서 재설정
+      this.updateApprovalOrder(reorderedList);
+      this.$emit('update:lists', reorderedList);
+    },
+
+    // 결재 순서 업데이트
+    updateApprovalOrder(approvalList) {
+      approvalList.forEach((item, index) => {
+        item.sanctnOrdr = index + 1;
+      });
+    },
+
+    // 결재자 삭제
+    removeApproval(itemIndex) {
+      this.$emit('delSanctn', itemIndex);
+    },
+
+    // 드래그 상태 초기화
+    resetDragState() {
+      this.currentDraggedIndex = null;
+      this.currentDropTarget = null;
+    },
   }
 };
 </script>
@@ -132,11 +195,17 @@
   background: white;
   border-radius: 8px;
   padding: 8px;
+  cursor: grab;
+}
+
+.draggable-item:active {
+  cursor: grabbing;
 }
 
 .being-dragged {
   opacity: 0.5;
   box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
+  transform: rotate(2deg);
 }
 
 .drop-zone {
@@ -177,6 +246,7 @@
 .drop-zone.drop-active {
   border-color: #007bff !important;
   background-color: #e3f2fd;
+  box-shadow: 0 0 10px rgba(0, 123, 255, 0.3);
 }
 
 .drop-indicator {
@@ -184,5 +254,14 @@
   font-weight: bold;
   font-size: inherit;
   pointer-events: none;
+  user-select: none;
+}
+
+.addapproval {
+  margin-bottom: 8px;
+}
+
+.addapproval:last-child {
+  margin-bottom: 0;
 }
 </style>
(파일 끝에 줄바꿈 문자 없음)
client/views/layout/LeftNavBar.vue
--- client/views/layout/LeftNavBar.vue
+++ client/views/layout/LeftNavBar.vue
@@ -76,11 +76,11 @@
         ]
       },
       {
-        name: 'hyuga',
+        name: 'vcatn',
         title: '휴가',
         items: [
-          { name: 'hyugaStatue', title: '휴가 현황' },
-          { name: 'hyugaInsert', title: '휴가 신청' }
+          { name: 'VcatnListPage', title: '휴가 현황' },
+          { name: 'VcatnInsertPage', title: '휴가 신청' }
         ]
       },
       {
@@ -212,7 +212,7 @@
       const routeChecks = {
         sanctn: () => this.currentRouteName.includes('approval'),
         attendance: () => ['attendance', 'myattendance', 'buseoattendance'].some(name => this.currentRouteName.includes(name)),
-        hyuga: () => this.currentRouteName.includes('hyuga'),
+        vcatn: () => this.currentRouteName.includes('vcatn'),
         bsrp: () => this.currentRouteName.includes('bsrp'),
         project: () => this.currentRouteName.includes('project'),
         salary: () => this.currentRouteName.includes('salary'),
client/views/pages/AppRouter.js
--- client/views/pages/AppRouter.js
+++ client/views/pages/AppRouter.js
@@ -9,16 +9,18 @@
 import MyApprovalRequestListComp from '../pages/Manager/sanctn/MyApprovalRequestList.vue';
 import PendingApprovalListComp from '../pages/Manager/sanctn/PendingApprovalList.vue';
 
-//근태관리
+// 근태관리
 import myAttendance from '../pages/Manager/attendance/myAttendance.vue';
 import buseoAttendance from '../pages/Manager/attendance/buseoAttendance.vue';
 import AttendanceDetail from '../pages/Manager/attendance/AttendanceDetail.vue';
-import hyugaStatue from '../pages/Manager/attendance/hyugaStatue.vue';
-import HyugaInsert from '../pages/Manager/attendance/HyugaInsert.vue';
-import HyugaDetail from '../pages/Manager/attendance/HyugaDetail.vue';
+// 근태관리 - 휴가
+import VcatnListComp from '../pages/Manager/attendance/VcatnList.vue';
+import VcatnViewComp from './Manager/attendance/VcatnView.vue';
+import VcatnInsertComp from '../pages/Manager/attendance/VcatnInsert.vue';
+// 근태관리 - 출장
 import BsrpListComp from '../pages/Manager/attendance/BsrpList.vue';
-import BsrpInsertComp from '../pages/Manager/attendance/BsrpInsert.vue';
 import BsrpViewComp from './Manager/attendance/BsrpView.vue';
+import BsrpInsertComp from '../pages/Manager/attendance/BsrpInsert.vue';
 import BsrpRportInsertComp from '../pages/Manager/attendance/BsrpRportInsert.vue';
 
 //업무관리
@@ -78,10 +80,10 @@
       { path: 'myAttendance.page', name: 'myAttendance', component: myAttendance },
       { path: 'buseoAttendance.page', name: 'buseoAttendance', component: buseoAttendance },
       { path: 'AttendanceDetail.page', name: 'AttendanceDetail', component: AttendanceDetail },
-      { path: 'hyugaStatue.page', name: 'hyugaStatue', component: hyugaStatue },
-      { path: 'HyugaDetail.page', name: 'HyugaDetail', component: HyugaDetail },
-      { path: 'hyugaInsert.page', name: 'hyugaInsert', component: HyugaInsert },
 
+      { path: 'VcatnList.page', name: 'VcatnListPage', component: VcatnListComp },
+      { path: 'VcatnView.page', name: 'VcatnViewPage', component: VcatnViewComp },
+      { path: 'VcatnInsert.page', name: 'VcatnInsertPage', component: VcatnInsertComp },
       { path: 'BsrpList.page', name: 'BsrpListPage', component: BsrpListComp },
       { path: 'BsrpView.page', name: 'BsrpViewPage', component: BsrpViewComp },
       { path: 'BsrpInsert.page', name: 'BsrpInsertPage', component: BsrpInsertComp },
 
client/views/pages/Manager/attendance/HyugaDetail.vue (deleted)
--- client/views/pages/Manager/attendance/HyugaDetail.vue
@@ -1,207 +0,0 @@
-<template>
-  <div class="card">
-    <div class="card-body">
-      <h2 class="card-title">휴가 현황</h2>
-      <div class="form-card">
-        <h1>휴가신청서</h1>
-        <SanctnViewList v-if="detailData.sanctnList.length > 0" :sanctns="detailData.sanctnList" />
-        <form class="row g-3 needs-validation detail" novalidate>
-          <div class="col-12">
-            <div class="col-12 border-x">
-              <label for="vcatnType" class="form-label">유형</label>
-              <p>{{ detailData.vcatnKndNm }}</p>
-            </div>
-            <div class="col-12 border-x">
-              <label for="applicant" class="form-label">신청자</label>
-              <p>{{ detailData.userNm }}</p>
-            </div>
-          </div>
-          <div class="col-12">
-            <div class="col-12 border-x">
-              <label for="department" class="form-label">부서</label>
-              <p>{{ detailData.deptNm }}</p>
-            </div>
-            <div class="col-12 border-x">
-              <label for="position" class="form-label">직급</label>
-              <p>{{ detailData.clsfNm }}</p>
-            </div>
-          </div>
-          <div class="col-12">
-            <label for="period" class="form-label">기간</label>
-            <p>{{ $formattedDates(detailData) }}</p>
-          </div>
-          <div class="col-12 hyuga">
-            <label for="details" class="form-label">세부사항</label>
-            <ViewerComponent :content="detailData.detailCn" />
-          </div>
-          <div class="col-12">
-            <label for="requestDate" class="form-label">신청일</label>
-            <p>{{ detailData.rgsde }}</p>
-          </div>
-          <div class="col-12">
-            <label for="status" class="form-label">상태</label>
-            <p>{{ detailData.confmAtNm }}</p>
-          </div>
-          <div v-if="detailData.confmAt === 'R'" class="col-12 border-x return">
-            <label for="rejectReason" class="form-label">반려사유</label>
-            <template v-for="(item, idx) of detailData.sanctnList" :key="idx">
-              <p v-if="item.confmAt === 'R'">{{ item.returnResn }}</p>
-            </template>
-          </div>
-        </form>
-      </div>
-      <div class="buttons">
-        <button v-if="detailData.confmAt === 'W' || detailData.confmAt === 'R'" class="btn sm btn-red" type="button" @click="deleteData">신청취소</button>
-        <button v-if="detailData.confmAt === 'W'" class="btn sm secondary" type="button" @click="fnMoveTo('edit', pageId)">수정</button>
-        <button v-if="detailData.confmAt === 'R'" class="btn sm secondary" type="button" @click="fnMoveTo('edit', pageId)">재신청</button>
-        <button class="btn sm tertiary" type="button" @click="fnMoveTo('list')">목록</button>
-      </div>
-      <ReturnPopup v-if="showPopup" @close="showPopup = false" />
-    </div>
-  </div>
-</template>
-<script>
-import ReturnPopup from '../../../component/Popup/ReturnPopup.vue';
-import ViewerComponent from '../../../component/editor/ViewerComponent.vue';
-import SanctnViewList from '../../../component/Sanctn/SanctnViewList.vue';
-// API
-import { findVcatnProc, deleteVcatnProc } from '../../../../resources/api/vcatn';
-
-export default {
-  components: {
-    ReturnPopup, ViewerComponent,
-    SanctnViewList,
-  },
-
-  data() {
-    return {
-      require: "/client/resources/img/require.png",
-
-      pageId: null,
-      showPopup: false,
-      isRegister: false,
-
-      detailData: {
-        vcatnId: null,
-        userId: null,
-        userNm: null,
-        vcatnKnd: null,
-        vcatnKndNm: null,
-        deptId: null,
-        deptNm: null,
-        clsf: null,
-        clsfNm: null,
-        bgnde: null,
-        beginHour: null,
-        beginMnt: null,
-        endde: null,
-        endHour: null,
-        endMnt: null,
-        detailCn: null,
-        confmAt: null,
-        confmAtNm: null,
-        rgsde: null,
-        register: null,
-        updde: null,
-        updusr: null,
-        sanctnList: []
-      },
-    };
-  },
-
-  computed: {},
-
-  async created() {
-    this.pageId = this.$route.query.id;
-    if (this.$isEmpty(this.pageId)) {
-      alert("게시물이 존재하지 않습니다.");
-      this.fnMoveTo('list');
-    }
-  },
-
-  mounted() {
-    this.findData(); // 상세 조회
-  },
-
-  methods: {
-    // 상세 조회
-    async findData() {
-      try {
-        const response = await findVcatnProc(this.pageId);
-        const result = response.data.data;
-
-        this.detailData = result.vo;
-        this.isRegister = this.$registerChk(result.vo.register);
-      } catch (error) {
-        if (error.response) {
-          alert(error.response.data.message);
-        } else {
-          alert("에러가 발생했습니다.");
-        }
-        console.error(error.message);
-        this.fnMoveTo('list');
-      }
-    },
-
-    // 일수 계산
-    calculateDayCount() {
-      if (!this.detailData.bgnde || !this.detailData.endde) return 0;
-
-      let dayCnt = 1; // 기본값
-
-      // 반차인 경우
-      if (this.detailData.vcatnKnd === 'MORNING_HALF' || this.detailData.vcatnKnd === 'AFTERNOON_HALF') {
-        dayCnt = 0.5;
-      }
-
-      const startDate = new Date(this.detailData.bgnde);
-      const endDate = new Date(this.detailData.endde);
-      const timeDiff = endDate.getTime() - startDate.getTime();
-      const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24)) + 1;
-
-      return dayCnt * Math.max(0, dayDiff);
-    },
-
-    // 삭제
-    async deleteData() {
-      const isCheck = confirm("삭제하시겠습니까?");
-      if (!isCheck) {
-        return;
-      }
-
-      try {
-        const response = await deleteVcatnProc(this.pageId);
-
-        this.fnMoveTo('list');
-      } catch (error) {
-        if (error.response) {
-          alert(error.response.data.message);
-        } else {
-          alert("에러가 발생했습니다.");
-        }
-        console.error(error.message);
-        this.fnMoveTo('list');
-      }
-    },
-
-    // 페이지 이동
-    fnMoveTo(type, id) {
-      const routes = {
-        'list': { name: 'hyugaStatue' },
-        'view': { name: 'HyugaDetail', query: { id } },
-        'edit': { name: 'hyugaInsert', query: this.$isEmpty(id) ? {} : { id } },
-      };
-
-      if (routes[type]) {
-        if (!this.$isEmpty(this.pageId) && type === 'list') {
-          this.$router.push({ name: 'HyugaDetail', query: { id: this.pageId } });
-        }
-        this.$router.push(routes[type]);
-      } else {
-        alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다.");
-        this.$router.push(routes['list']);
-      }
-    },
-  }
-};
-</script>(파일 끝에 줄바꿈 문자 없음)
 
client/views/pages/Manager/attendance/HyugaInsert.vue (deleted)
--- client/views/pages/Manager/attendance/HyugaInsert.vue
@@ -1,487 +0,0 @@
-<template>
-  <div class="card">
-    <div class="card-body">
-      <h2 class="card-title">휴가 신청</h2>
-      <p class="require"><img :src="require" alt=""> 필수입력</p>
-      <div class="tbl-wrap">
-        <table class="tbl data">
-          <tbody>
-            <tr>
-              <th scope="row">
-                유형 <span class="require"><img :src="require" alt=""></span>
-              </th>
-              <td>
-                <select class="form-select sm" style="max-width: 200px;" v-model="editData.vcatnKnd" @change="fnOnchangeVcatnKnd">
-                  <option value="" disabled hidden>유형 선택</option>
-                  <option v-for="(item, idx) of vcatnKnds" :key="idx" :value="item.code">{{ item.codeNm }}</option>
-                </select>
-              </td>
-            </tr>
-            <tr>
-              <th scope="row">
-                시작일 <span class="require"><img :src="require" alt=""></span>
-              </th>
-              <td>
-                <div class="d-flex gap-1">
-                  <input type="date" class="form-control sm" v-model="editData.bgnde" @keydown="preventKeyboard" />
-                  <input type="text" class="form-control sm" placeholder="시" style="width: 100px;" v-model="editData.beginHour" readonly />
-                  <input type="text" class="form-control sm" placeholder="분" style="width: 100px;" v-model="editData.beginMnt" readonly />
-                </div>
-              </td>
-            </tr>
-            <tr>
-              <th scope="row">
-                종료일 <span class="require"><img :src="require" alt=""></span>
-              </th>
-              <td>
-                <div class="d-flex gap-1">
-                  <input type="date" class="form-control sm" v-model="editData.endde" :readonly="dayCnt === 0.5" @keydown="preventKeyboard" />
-                  <input type="text" class="form-control sm" placeholder="시" style="width: 100px;" v-model="editData.endHour" readonly />
-                  <input type="text" class="form-control sm" placeholder="분" style="width: 100px;" v-model="editData.endMnt" readonly />
-                </div>
-              </td>
-            </tr>
-            <tr>
-              <th scope="row">사용 휴가일</th>
-              <td>
-                <input type="text" class="form-control sm" v-model="totalDays" readonly />
-              </td>
-            </tr>
-            <tr>
-              <th scope="row">
-                승인자 <span class="require"><img :src="require" alt=""></span>
-              </th>
-              <td>
-                <button type="button" title="추가" @click="isOpenModal = true">
-                  <PlusCircleFilled />
-                </button>
-                <HrPopup v-if="isOpenModal" :lists="editData.sanctnList" @onSelected="fnAddSanctn" @close="isOpenModal = false" />
-                <div class="approval-container">
-                  <SanctnList v-model:lists="editData.sanctnList" @delSanctn="fnDelSanctn" />
-                </div>
-              </td>
-            </tr>
-            <tr>
-              <th scope="row">세부사항</th>
-              <td>
-                <EditorComponent v-model:contents="editData.detailCn" />
-              </td>
-            </tr>
-          </tbody>
-        </table>
-      </div>
-
-      <div class="buttons">
-        <button type="button" class="btn sm btn-red" @click="fnRecord">이전 승인자 불러오기</button>
-        <button type="button" class="btn sm primary" @click="fnSave">신청</button>
-        <button type="button" class="btn sm tertiary" @click="fnMoveTo('list')">취소</button>
-      </div>
-    </div>
-  </div>
-</template>
-<script>
-import { PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue';
-import HrPopup from '../../../component/Popup/HrPopup.vue';
-import SanctnList from '../../../component/Sanctn/SanctnFormList.vue';
-import EditorComponent from '../../../component/editor/EditorComponent.vue';
-// API
-import { findVcatnProc, saveVcatnProc, findLastVcatnProc, updateVcatnProc } from '../../../../resources/api/vcatn';
-
-export default {
-  components: {
-    PlusCircleFilled,
-    CloseOutlined,
-    HrPopup,
-    SanctnList,
-    EditorComponent,
-  },
-
-  data() {
-    return {
-      require: "/client/resources/img/require.png",
-
-      pageId: null,
-      isOpenModal: false,
-
-      editData: {
-        vcatnId: null,
-        userId: null,
-        vcatnKnd: null,
-        deptId: null,
-        clsf: null,
-        bgnde: null,
-        beginHour: null,
-        beginMnt: null,
-        endde: null,
-        endHour: null,
-        endMnt: null,
-        detailCn: null,
-        confmAt: null,
-        rgsde: null,
-        register: null,
-        updde: null,
-        updusr: null,
-        sanctnList: []
-      },
-      dayCnt: 0,
-      totalDays: 0,
-      workConfig: {
-        startHour: 9,    // 근무 시작 시간
-        endHour: 18,     // 근무 종료 시간
-        lunchStart: 12,  // 점심 시작 시간
-        lunchEnd: 13,    // 점심 종료 시간
-      },
-
-      vcatnKnds: [],
-      halfDaySubTypes: [],
-      vcatnSubKnd: null,
-
-      sanctnCodes: [],
-    };
-  },
-
-  computed: {
-    showSubTypeSelect() {
-      return this.dayCnt === 0.5;
-    },
-
-    // 현재 선택된 유형이 반차 하위 유형인지 확인
-    isHalfDaySubType() {
-      return this.halfDaySubTypes.some(item => item.code === this.editData.vcatnKnd);
-    }
-  },
-
-  async created() {
-    this.pageId = this.$route.query.id;
-
-    // 휴가 유형 조회
-    this.vcatnKnds = []; // 초기화
-    let halfDaySubTypes = [];
-    const vcatnKndCodes = await this.$findChildCodes('sanctn_mby_vcatn');
-    for (const code of vcatnKndCodes) {
-      const childCodes = await this.$findChildCodes(code.code);
-      for (const childCode of childCodes) {
-        if (parseFloat(childCode.codeValue) === 0.5) {
-          // 반차(0.5)인 경우 해당 유형은 제외하고 하위 코드만 추가
-          const subTypes = await this.$findChildCodes(childCode.code);
-          this.vcatnKnds.push(...subTypes);
-          halfDaySubTypes.push(...subTypes);
-        } else {
-          // 반차가 아닌 경우 그대로 추가
-          this.vcatnKnds.push(childCode);
-        }
-      }
-    }
-    this.halfDaySubTypes = halfDaySubTypes;
-
-    if (this.vcatnKnds.length > 0) {
-      this.editData.vcatnKnd = this.vcatnKnds[0].code;
-    }
-
-    // 결재 구분
-    this.sanctnCodes = await this.$findChildCodes('sanctn_code');
-
-    // 상세 조회 (pageId가 있는 경우)
-    if (!this.$isEmpty(this.pageId)) {
-      await this.findData();
-    }
-  },
-
-  mounted() { },
-
-  watch: {
-    'editData.bgnde'(newVal, oldVal) {
-      if (newVal !== oldVal) {
-        this.changeBgnde();
-      }
-    },
-    'editData.endde'(newVal, oldVal) {
-      if (newVal !== oldVal) {
-        this.validateAndCalculateDays();
-      }
-    },
-  },
-
-  methods: {
-    // 상세 조회
-    async findData() {
-      try {
-        const response = await findVcatnProc(this.pageId);
-        const result = response.data.data;
-
-        this.editData = result.vo;
-        this.editData.bgnde = this.editData.bgnde.split(' ')[0];
-        this.editData.endde = this.editData.endde.split(' ')[0];
-
-        await this.$nextTick();
-        await this.fnOnchangeVcatnKnd();
-
-      } catch (error) {
-        console.error('데이터 조회 실패:', error);
-        const message = error.response?.data?.message || "데이터를 불러오는데 실패했습니다.";
-        alert(message);
-        this.fnMoveTo('list');
-      }
-    },
-
-    // 유형 변경
-    async fnOnchangeVcatnKnd() {
-      const selectedVcatn = this.vcatnKnds.find(item => item.code === this.editData.vcatnKnd);
-      if (!selectedVcatn) {
-        console.warn('선택된 휴가 유형을 찾을 수 없습니다.');
-        return;
-      }
-
-      // 반차 하위 유형인지 확인
-      if (this.isHalfDaySubType) {
-        this.dayCnt = 0.5;
-        this.setHalfDayTime(selectedVcatn);
-        this.changeBgnde(); // 반차일 경우 종료일을 시작일과 동일하게 설정
-      } else {
-        this.dayCnt = parseFloat(selectedVcatn.codeValue);
-
-        // 전체 근무시간으로 설정
-        this.editData.beginHour = this.workConfig.startHour.toString().padStart(2, '0');
-        this.editData.beginMnt = '00';
-        this.editData.endHour = this.workConfig.endHour.toString().padStart(2, '0');
-        this.editData.endMnt = '00';
-        this.calculateTotalDays();
-      }
-    },
-
-    // 반차 시간 설정 메서드
-    setHalfDayTime(selectedSubType) {
-      const codeValue = selectedSubType.codeValue;
-
-      // 전체 실제 근무시간 계산 (점심시간 제외)
-      const totalHours = this.workConfig.endHour - this.workConfig.startHour; // 전체 시간
-      const lunchHours = this.workConfig.lunchEnd - this.workConfig.lunchStart; // 점심시간
-      const actualWorkHours = totalHours - lunchHours; // 실제 근무시간
-      const halfWorkHours = actualWorkHours / 2; // 반차 시간
-
-      this.editData.beginMnt = '00';
-      this.editData.endMnt = '00';
-
-      if (codeValue === 'AM') {
-        this.editData.beginHour = this.workConfig.startHour.toString().padStart(2, '0');
-        this.editData.endHour = this.calculateTimeWithLunch(this.workConfig.startHour, halfWorkHours, 1).toString().padStart(2, '0');
-      } else if (codeValue === 'PM') {
-        this.editData.beginHour = this.calculateTimeWithLunch(this.workConfig.endHour, halfWorkHours, -1).toString().padStart(2, '0');
-        this.editData.endHour = this.workConfig.endHour.toString().padStart(2, '0');
-      }
-    },
-
-    // 점심시간을 고려하여 시간 계산 (방향: 1=앞으로, -1=뒤로)
-    calculateTimeWithLunch(startTime, workHours, direction = 1) {
-      let currentHour = startTime;
-      let remainingHours = workHours;
-
-      while (remainingHours > 0) {
-        if (direction === 1) {
-          // 앞으로 계산 (종료시간 구하기)
-          if (currentHour >= this.workConfig.lunchStart && currentHour < this.workConfig.lunchEnd) {
-            currentHour++;
-          } else {
-            currentHour++;
-            remainingHours--;
-          }
-        } else {
-          // 뒤로 계산 (시작시간 구하기)
-          currentHour--;
-          if (!(currentHour >= this.workConfig.lunchStart && currentHour < this.workConfig.lunchEnd)) {
-            remainingHours--;
-          }
-        }
-      }
-
-      return currentHour;
-    },
-
-    // 시작일과 종료일 통일
-    changeBgnde() {
-      if (!this.editData.bgnde) {
-        return;
-      }
-
-      // 반차인 경우 종료일을 시작일과 동일하게 설정
-      if (this.dayCnt === 0.5) {
-        this.editData.endde = this.editData.bgnde;
-      }
-
-      this.validateAndCalculateDays(); // 사용 휴가일 계산
-    },
-
-    // 사용 휴가일 계산
-    validateAndCalculateDays() {
-      if (!this.editData.bgnde || !this.editData.endde) {
-        this.totalDays = 0;
-        return;
-      }
-
-      const startDate = new Date(this.editData.bgnde);
-      const endDate = new Date(this.editData.endde);
-
-      // 날짜 유효성 검사
-      if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
-        this.totalDays = 0;
-        return;
-      }
-
-      // 종료일이 시작일보다 이전인지 확인 (반차가 아닌 경우)
-      if (this.dayCnt !== 0.5 && endDate < startDate) {
-        alert('종료일은 시작일보다 이전일 수 없습니다.');
-        this.editData.endde = this.editData.bgnde;
-        return;
-      }
-
-      this.calculateTotalDays();
-    },
-
-    calculateTotalDays() {
-      if (!this.editData.bgnde || !this.editData.endde) {
-        this.totalDays = 0;
-        return;
-      }
-
-      const startDate = new Date(this.editData.bgnde);
-      const endDate = new Date(this.editData.endde);
-
-      if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
-        this.totalDays = 0;
-        return;
-      }
-
-      const timeDiff = endDate.getTime() - startDate.getTime();
-      const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24)) + 1;
-
-      // 반차의 경우 0.5일, 그 외의 경우 실제 일수 계산
-      if (this.dayCnt === 0.5) {
-        this.totalDays = 0.5;
-      } else {
-        this.totalDays = Math.max(0, dayDiff);
-      }
-    },
-
-    preventKeyboard(event) {
-      if (event.key !== 'Tab') {
-        event.preventDefault();
-      }
-    },
-
-    // 승인자
-    fnAddSanctn(user) {
-      const data = {
-        sanctnId: null,                                   // 결재 아이디
-        confmerId: user.userId,                           // 승인자 아이디
-        clsf: user.clsf,                                  // 직급
-        sanctnOrdr: this.editData.sanctnList.length + 1,  // 결재 순서
-        sanctnIem: this.editData.vcatnKnd,                // 결재 항목
-        sanctnMbyId: null,                                // 결재 주체 아이디
-        sanctnSe: this.sanctnCodes[0].code,               // 결재구분
-
-        confmerNm: user.userNm,                           // 승인자 이름
-        clsfNm: user.clsfNm,                              // 직급 이름
-      };
-
-      this.editData.sanctnList.push(data);
-      this.isOpenModal = false;
-    },
-
-    fnDelSanctn(idx) {
-      this.editData.sanctnList.splice(idx, 1);
-      this.editData.sanctnList.forEach((item, index) => {
-        item.sanctnOrdr = index + 1;
-      });
-    },
-
-    // 이전 승인자 조회
-    async fnRecord() {
-      try {
-        const response = await findLastVcatnProc();
-        const result = response.data.data;
-
-        if (this.$isEmpty(result.lists)) {
-          alert("휴가 기록이 존재하지 않아, 이전 승인자를 불러올 수 없습니다.");
-          return;
-        }
-
-        this.editData.sanctnList = result.lists;
-      } catch (error) {
-        console.error('이전 승인자 조회 실패:', error);
-        const message = error.response?.data?.message || "이전 승인자를 불러오는데 실패했습니다.";
-        alert(message);
-      }
-    },
-
-    // 유효성 검사
-    validateForm() {
-      if (this.$isEmpty(this.editData.vcatnKnd)) {
-        alert("유형을 선택해 주세요.");
-        return false;
-      }
-
-      if (this.$isEmpty(this.editData.bgnde)) {
-        alert("시작일을 선택해 주세요.");
-        return false;
-      }
-
-      if (this.$isEmpty(this.editData.endde)) {
-        alert("종료일을 선택해 주세요.");
-        return false;
-      }
-
-      if (this.$isEmpty(this.editData.sanctnList)) {
-        alert("승인자를 선택해 주세요.");
-        return false;
-      }
-
-      return true;
-    },
-
-    // 신청
-    async fnSave() {
-      try {
-        if (!this.validateForm()) {
-          return;
-        }
-
-        // 데이터 세팅
-        let data = this.editData;
-        if (!this.$isEmpty(this.pageId)) {
-          data.confmAt = 'W';
-        }
-
-        const response = this.$isEmpty(this.pageId) ? await saveVcatnProc(data) : await updateVcatnProc(data);
-        const message = this.$isEmpty(this.pageId) ? "등록되었습니다." : "수정되었습니다.";
-        alert(message);
-
-        this.fnMoveTo('view', response.data.data.pageId);
-      } catch (error) {
-        console.error('저장 실패:', error);
-        const message = error.response.data.message || "저장에 실패했습니다.";
-        alert(message);
-      }
-    },
-
-    // 페이지 이동
-    fnMoveTo(type, id) {
-      const routes = {
-        'list': { name: 'hyugaStatue' },
-        'view': { name: 'HyugaDetail', query: { id } },
-        'edit': { name: 'hyugaInsert', query: this.$isEmpty(id) ? {} : { id } },
-      };
-
-      if (routes[type]) {
-        if (!this.$isEmpty(this.pageId) && type === 'list') {
-          this.$router.push({ name: 'HyugaDetail', query: { id: this.pageId } });
-          return;
-        }
-        this.$router.push(routes[type]);
-      } else {
-        alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다.");
-        this.$router.push(routes['list']);
-      }
-    },
-  },
-};
-</script>(파일 끝에 줄바꿈 문자 없음)
 
client/views/pages/Manager/attendance/VcatnInsert.vue (added)
+++ client/views/pages/Manager/attendance/VcatnInsert.vue
@@ -0,0 +1,543 @@
+<template>
+  <div class="card">
+    <div class="card-body">
+      <h2 class="card-title">휴가 신청</h2>
+      <p class="require">* 필수입력</p>
+      <div class="tbl-wrap">
+        <table class="tbl data">
+          <tbody>
+            <tr>
+              <th scope="row">유형 *</th>
+              <td>
+                <select class="form-select sm" v-model="vacationInfo.vcatnKnd" @change="handleVacationTypeChange">
+                  <option value="" disabled hidden>유형 선택</option>
+                  <option v-for="(item, idx) of vacationTypes" :key="idx" :value="item.code">{{ item.codeNm }}</option>
+                </select>
+              </td>
+            </tr>
+            <tr>
+              <th scope="row">시작일 *</th>
+              <td>
+                <div class="d-flex gap-1">
+                  <input type="date" class="form-control sm" v-model="vacationInfo.bgnde" @keydown="preventKeyboard" />
+                  <input type="text" class="form-control sm" placeholder="시" style="width: 100px;" v-model="vacationInfo.beginHour" readonly />
+                  <input type="text" class="form-control sm" placeholder="분" style="width: 100px;" v-model="vacationInfo.beginMnt" readonly />
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th scope="row">종료일 *</th>
+              <td>
+                <div class="d-flex gap-1">
+                  <input type="date" class="form-control sm" v-model="vacationInfo.endde" :readonly="vacationDayCount === 0.5" @keydown="preventKeyboard" />
+                  <input type="text" class="form-control sm" placeholder="시" style="width: 100px;" v-model="vacationInfo.endHour" readonly />
+                  <input type="text" class="form-control sm" placeholder="분" style="width: 100px;" v-model="vacationInfo.endMnt" readonly />
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th scope="row">사용 휴가일</th>
+              <td>
+                <input type="text" class="form-control sm" v-model="totalDays" readonly />
+              </td>
+            </tr>
+            <tr>
+              <th scope="row">승인자 *</th>
+              <td>
+                <button type="button" title="추가" @click="isOpenApproverModal = true">
+                  <PlusCircleFilled />
+                </button>
+                <HrPopup v-if="isOpenApproverModal" :selectedEmployees="vacationCnsul.sanctnList" idField="confmerId" @select="handleApproverAdd" @close="isOpenApproverModal = false" />
+                <div class="approval-container">
+                  <SanctnList v-model:lists="vacationCnsul.sanctnList" @delSanctn="handleApproverRemove" />
+                </div>
+              </td>
+            </tr>
+            <tr>
+              <th scope="row">세부사항</th>
+              <td>
+                <EditorComponent v-model:contents="vacationCnsul.detailCn" />
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+      <div class="buttons">
+        <button type="button" class="btn sm btn-red" @click="handleLoadLastApprovers">이전 승인자 불러오기</button>
+        <button type="button" class="btn sm primary" v-if="$isEmpty(pageId)" @click="handleSave">신청</button>
+        <template v-else>
+          <button type="button" class="btn sm primary" v-if="isReapplyMode" @click="handleReapply">재신청</button>
+          <button type="button" class="btn sm primary" v-else @click="handleUpdate">수정</button>
+          <button type="button" class="btn sm secondary" @click="handleCancel">취소</button>
+        </template>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import { PlusCircleFilled } from '@ant-design/icons-vue';
+import HrPopup from '../../../component/Popup/HrPopup.vue';
+import SanctnList from '../../../component/Sanctn/SanctnFormList.vue';
+import EditorComponent from '../../../component/editor/EditorComponent.vue';
+// API
+import { findVcatnKndsProc, findVcatnProc, saveVcatnProc, findLastVcatnProc, updateVcatnProc, reapplyVcatnProc } from '../../../../resources/api/vcatn';
+
+export default {
+  name: 'VacationInsert',
+
+  components: {
+    PlusCircleFilled,
+    HrPopup,
+    SanctnList,
+    EditorComponent,
+  },
+
+  data() {
+    return {
+      pageId: null,
+      pageMode: null,
+      isOpenApproverModal: false,
+      vacationDayCount: 0,
+      totalDays: 0,
+      workConfig: {
+        startHour: 9,
+        endHour: 18,
+        lunchStart: 12,
+        lunchEnd: 13,
+      },
+      vacationInfo: {
+        vcatnId: null,
+        userId: null,
+        vcatnKnd: '',
+        deptId: null,
+        clsf: null,
+        bgnde: null,
+        beginHour: null,
+        beginMnt: null,
+        endde: null,
+        endHour: null,
+        endMnt: null,
+        confmAt: null,
+        rgsde: null,
+        register: null,
+        updde: null,
+        updusr: null,
+      },
+      vacationCnsul: {
+        detailCn: null,
+        sanctnList: []
+      },
+      vacationTypes: [],
+      approvalCodes: [],
+    };
+  },
+
+  watch: {
+    // 시작일 변동 시 날짜 재계산
+    'vacationInfo.bgnde'(newVal, oldVal) {
+      if (newVal !== oldVal) {
+        this.handleStartDateChange();
+      }
+    },
+    // 종료일 변동 시 날짜 재계산
+    'vacationInfo.endde'(newVal, oldVal) {
+      if (newVal !== oldVal) {
+        this.validateAndCalculateDays();
+      }
+    },
+  },
+
+  computed: {
+    // 재신청 여부
+    isReapplyMode() {
+      return this.pageMode === 'reapply';
+    },
+  },
+
+  async created() {
+    this.pageId = this.$route.query.id;
+    this.pageMode = this.$route.query.type;
+
+    await this.fetchVacationTypes(); // 휴가 유형 조회
+    this.approvalCodes = await this.$findChildCodes('sanctn_code'); // 결재 구분 조회
+  },
+
+  mounted() {
+    if (!this.$isEmpty(this.pageId)) {
+      this.fetchData(); // 상세 조회
+    }
+  },
+
+  methods: {
+    // 휴가 유형 조회
+    async fetchVacationTypes() {
+      try {
+        const response = await findVcatnKndsProc();
+        const result = response.data.data;
+
+        this.vacationTypes = result;
+      } catch (error) {
+        this.handleError(error);
+        this.handleNavigation('list');
+      }
+    },
+
+    // 휴가 정보 조회
+    async fetchData() {
+      try {
+        const response = await findVcatnProc(this.pageId);
+        const result = response.data.data;
+
+        this.vacationInfo = {
+          vcatnId: result.vcatnId,
+          userId: result.userId,
+          vcatnKnd: result.vcatnKnd,
+          deptId: result.deptId,
+          clsf: result.clsf,
+          bgnde: result.bgnde,
+          beginHour: result.beginHour,
+          beginMnt: result.beginMnt,
+          endde: result.endde,
+          endHour: result.endHour,
+          endMnt: result.endMnt,
+          confmAt: result.confmAt,
+          rgsde: result.rgsde,
+          register: result.register,
+          updde: result.updde,
+          updusr: result.updusr,
+        };
+        this.vacationCnsul = {
+          detailCn: result.detailCn,
+          sanctnList: result.sanctnList
+        };
+      } catch (error) {
+        this.handleError(error);
+        this.handleNavigation('list');
+      }
+    },
+
+    // 휴가 유형 변경 핸들러
+    handleVacationTypeChange() {
+      const selectedVacation = this.vacationTypes.find(item => item.code === this.vacationInfo.vcatnKnd);
+      if (!selectedVacation) {
+        console.warn('선택된 휴가 유형을 찾을 수 없습니다.');
+        return;
+      }
+
+      if (selectedVacation.codeValue != 1) {
+        this.setHalfDayTime(selectedVacation.codeValue);
+        this.handleStartDateChange(); // 반차일 경우 종료일을 시작일과 동일하게 설정
+      } else {
+        this.vacationDayCount = parseFloat(selectedVacation.codeValue);
+
+        // 전체 근무시간으로 설정
+        this.vacationInfo.beginHour = this.workConfig.startHour.toString().padStart(2, '0');
+        this.vacationInfo.beginMnt = '00';
+        this.vacationInfo.endHour = this.workConfig.endHour.toString().padStart(2, '0');
+        this.vacationInfo.endMnt = '00';
+        this.calculateTotalDays();
+      }
+    },
+
+    // 반차 시간 설정
+    setHalfDayTime(codeValue) {
+      // 전체 실제 근무시간 계산 (점심시간 제외)
+      const totalHours = this.workConfig.endHour - this.workConfig.startHour;
+      const lunchHours = this.workConfig.lunchEnd - this.workConfig.lunchStart;
+      const actualWorkHours = totalHours - lunchHours;
+      const halfWorkHours = actualWorkHours / 2;
+
+      this.vacationInfo.beginMnt = '00';
+      this.vacationInfo.endMnt = '00';
+
+      if (codeValue === 'AM') {
+        this.vacationDayCount = 0.5;
+        this.vacationInfo.beginHour = this.workConfig.startHour.toString().padStart(2, '0');
+        this.vacationInfo.endHour = this.calculateTimeWithLunch(this.workConfig.startHour, halfWorkHours, 1).toString().padStart(2, '0');
+      } else if (codeValue === 'PM') {
+        this.vacationDayCount = 0.5;
+        this.vacationInfo.beginHour = this.calculateTimeWithLunch(this.workConfig.endHour, halfWorkHours, -1).toString().padStart(2, '0');
+        this.vacationInfo.endHour = this.workConfig.endHour.toString().padStart(2, '0');
+      }
+    },
+
+    // 점심시간을 고려하여 시간 계산 (방향: 1=앞으로, -1=뒤로)
+    calculateTimeWithLunch(startTime, workHours, direction = 1) {
+      let currentHour = startTime;
+      let remainingHours = workHours;
+
+      while (remainingHours > 0) {
+        if (direction === 1) {
+          // 앞으로 계산 (종료시간 구하기)
+          if (currentHour >= this.workConfig.lunchStart && currentHour < this.workConfig.lunchEnd) {
+            currentHour++;
+          } else {
+            currentHour++;
+            remainingHours--;
+          }
+        } else {
+          // 뒤로 계산 (시작시간 구하기)
+          currentHour--;
+          if (!(currentHour >= this.workConfig.lunchStart && currentHour < this.workConfig.lunchEnd)) {
+            remainingHours--;
+          }
+        }
+      }
+
+      return currentHour;
+    },
+
+    // 시작일 변경 핸들러
+    handleStartDateChange() {
+      if (!this.vacationInfo.bgnde) {
+        return;
+      }
+
+      // 반차인 경우 종료일을 시작일과 동일하게 설정
+      if (this.vacationDayCount < 1) {
+        this.vacationInfo.endde = this.vacationInfo.bgnde;
+      }
+
+      this.validateAndCalculateDays();
+    },
+
+    // 사용 휴가일 유효성 검사 및 계산
+    validateAndCalculateDays() {
+      if (!this.vacationInfo.bgnde || !this.vacationInfo.endde) {
+        this.totalDays = 0;
+        return;
+      }
+
+      const startDate = new Date(this.vacationInfo.bgnde);
+      const endDate = new Date(this.vacationInfo.endde);
+
+      // 날짜 유효성 검사
+      if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
+        this.totalDays = 0;
+        return;
+      }
+
+      // 종료일이 시작일보다 이전인지 확인 (반차가 아닌 경우)
+      if (this.vacationDayCount == 1 && endDate < startDate) {
+        alert('종료일은 시작일보다 이전일 수 없습니다.');
+        this.vacationInfo.endde = this.vacationInfo.bgnde;
+        return;
+      }
+
+      this.calculateTotalDays();
+    },
+
+    // 총 휴가일 계산
+    calculateTotalDays() {
+      if (!this.vacationInfo.bgnde || !this.vacationInfo.endde) {
+        this.totalDays = 0;
+        return;
+      }
+
+      const startDate = new Date(this.vacationInfo.bgnde);
+      const endDate = new Date(this.vacationInfo.endde);
+
+      if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
+        this.totalDays = 0;
+        return;
+      }
+
+      const timeDiff = endDate.getTime() - startDate.getTime();
+      const dayDiff = Math.floor(timeDiff / (1000 * 60 * 60 * 24)) + 1;
+
+      // 반차의 경우 0.5일, 그 외의 경우 실제 일수 계산
+      if (this.vacationDayCount === 0.5) {
+        this.totalDays = 0.5;
+      } else {
+        this.totalDays = Math.max(0, dayDiff);
+      }
+    },
+
+    // 키보드 입력 방지
+    preventKeyboard(event) {
+      if (event.key !== 'Tab') {
+        event.preventDefault();
+      }
+    },
+
+    // 승인자 추가 핸들러
+    handleApproverAdd(user) {
+      const data = {
+        confmerId: user.userId,
+        confmerNm: user.userNm,
+        clsf: user.clsf,
+        clsfNm: user.clsfNm,
+        sanctnOrdr: this.vacationCnsul.sanctnList.length + 1,
+        sanctnIem: 'sanctn_mby_vcatn',
+        sanctnSe: this.approvalCodes[0].code,
+      };
+
+      this.vacationCnsul.sanctnList.push(data);
+      this.isOpenApproverModal = false;
+    },
+
+    // 승인자 삭제 핸들러
+    handleApproverRemove(idx) {
+      this.vacationCnsul.sanctnList.splice(idx, 1);
+      this.vacationCnsul.sanctnList.forEach((item, index) => {
+        item.sanctnOrdr = index + 1;
+      });
+    },
+
+    // 이전 승인자 불러오기 핸들러
+    async handleLoadLastApprovers() {
+      try {
+        const response = await findLastVcatnProc();
+        const result = response.data.data;
+
+        if (this.$isEmpty(result)) {
+          alert("휴가 기록이 존재하지 않아, 이전 승인자를 불러올 수 없습니다.");
+          return;
+        }
+
+        this.vacationCnsul.sanctnList = result;
+      } catch (error) {
+        this.handleError(error);
+      }
+    },
+
+    // 저장 핸들러
+    async handleSave() {
+      try {
+        if (!this.validateForm()) {
+          return;
+        }
+
+        const data = this.buildSendData();
+
+        const response = await saveVcatnProc(data);
+        alert("등록되었습니다.");
+
+        this.handleNavigation('view', response.data.data.pageId);
+      } catch (error) {
+        this.handleError(error);
+      }
+    },
+
+    // 수정 핸들러
+    async handleUpdate() {
+      try {
+        if (!this.validateForm()) {
+          return;
+        }
+
+        const data = this.buildSendData();
+
+        const response = await updateVcatnProc(this.pageId, data);
+        alert("수정되었습니다.");
+
+        this.handleNavigation('view', response.data.data.pageId);
+      } catch (error) {
+        this.handleError(error);
+      }
+    },
+
+    // 재신청 핸들러
+    async handleReapply() {
+      try {
+        if (!this.validateForm()) {
+          return;
+        }
+
+        const data = this.buildSendData();
+
+        const response = await reapplyVcatnProc(this.pageId, data);
+        alert("재신청되었습니다.");
+
+        this.handleNavigation('view', response.data.data.pageId);
+      } catch (error) {
+        this.handleError(error);
+      }
+    },
+
+    // 취소 핸들러
+    handleCancel() {
+      if (confirm('작성 중인 내용이 삭제됩니다. 계속하시겠습니까?')) {
+        this.handleNavigation('view', this.pageId);
+      }
+    },
+
+    // 페이지 이동 핸들러
+    handleNavigation(type, id) {
+      const routeMap = {
+        'list': { name: this.pageMode === 'sanctns' ? 'PendingApprovalListPage' : 'VcatnListPage' },
+        'view': { name: 'VcatnViewPage', query: { id } },
+        'insert': { name: 'VcatnInsertPage', query: this.$isEmpty(id) ? {} : { id } },
+        'reapply': { name: 'VcatnInsertPage', query: { id, type: 'reapply' } },
+      };
+
+      const route = routeMap[type];
+      if (route) {
+        this.$router.push(route);
+      } else {
+        alert("올바르지 않은 경로입니다.");
+        this.$router.push(routeMap['list']);
+      }
+    },
+
+    // 에러 핸들러
+    handleError(error) {
+      const message = error.response?.data?.message || '에러가 발생했습니다.';
+      alert(message);
+    },
+
+    // 입력값 전체 유효성 검사 유틸리티
+    validateForm() {
+      if (this.$isEmpty(this.vacationInfo.vcatnKnd)) {
+        alert("유형을 선택해 주세요.");
+        return false;
+      }
+
+      if (this.$isEmpty(this.vacationInfo.bgnde)) {
+        alert("시작일을 선택해 주세요.");
+        return false;
+      }
+
+      if (this.$isEmpty(this.vacationInfo.endde)) {
+        alert("종료일을 선택해 주세요.");
+        return false;
+      }
+
+      if (this.$isEmpty(this.vacationCnsul.sanctnList)) {
+        alert("승인자를 선택해 주세요.");
+        return false;
+      }
+
+      return true;
+    },
+
+    // 데이터 가공 유틸리티
+    buildSendData() {
+      const sanctnList = [];
+      for (let sanctn of this.vacationCnsul.sanctnList) {
+        sanctnList.push({
+          confmerId: sanctn.confmerId,
+          clsf: sanctn.clsf,
+          sanctnOrdr: sanctn.sanctnOrdr,
+          sanctnIem: sanctn.sanctnIem,
+          sanctnMbyId: sanctn.sanctnMbyId,
+          sanctnSe: sanctn.sanctnSe,
+        });
+      }
+
+      return {
+        userId: this.vacationInfo.userId,
+        vcatnKnd: this.vacationInfo.vcatnKnd,
+        bgnde: this.vacationInfo.bgnde,
+        beginHour: this.vacationInfo.beginHour,
+        beginMnt: this.vacationInfo.beginMnt,
+        endde: this.vacationInfo.endde,
+        endHour: this.vacationInfo.endHour,
+        endMnt: this.vacationInfo.endMnt,
+        detailCn: this.vacationCnsul.detailCn,
+        sanctnList: sanctnList,
+      };
+    },
+  },
+};
+</script>(파일 끝에 줄바꿈 문자 없음)
 
client/views/pages/Manager/attendance/VcatnList.vue (added)
+++ client/views/pages/Manager/attendance/VcatnList.vue
@@ -0,0 +1,237 @@
+<template>
+  <div class="col-lg-12">
+    <div class="card">
+      <div class="card-body">
+        <h2 class="card-title">휴가 현황</h2>
+        <div class="form-group">
+          <div class="form-conts">
+            <div class="form-conts datepicker-conts">
+              <div class="datepicker-input">
+                <input type="date" class="form-control datepicker cal" v-model="searchParams.bgnde" style="max-width: 200px;" @change="handleSearchChange" />
+                <mark>~</mark>
+                <input type="date" class="form-control datepicker cal" v-model="searchParams.endde" style="max-width: 200px;" @change="handleSearchChange" />
+              </div>
+            </div>
+          </div>
+        </div>
+        <div class="boxs">
+          <div class="color-boxs">
+            <div class="box" @click="handleFilterChange()">
+              <h3>전체</h3>
+              <div>
+                <span>{{ totalDays }}</span>
+                <small>({{ carriedOverDays }})</small>
+              </div>
+            </div>
+            <div class="box" @click="handleFilterChange(usedLeaveItem.code)">
+              <h3>사용</h3>
+              <div>{{ usedLeaveItem.userSpendCnt }}</div>
+            </div>
+            <div class="box">
+              <h3>미사용</h3>
+              <div>{{ remainingDays }}</div>
+            </div>
+            <div class="box" v-for="(item, idx) of otherItems" :key="idx" @click="handleFilterChange(item.code)">
+              <h3>{{ item.codeNm }}</h3>
+              <div>{{ item.userSpendCnt }}</div>
+            </div>
+          </div>
+        </div>
+        <div class="tbl-wrap">
+          <table id="myTable" class="tbl data">
+            <colgroup>
+              <col style="width: 15%;">
+              <col style="width: 40%;">
+              <col style="width: 30%;">
+              <col style="width: 15%;">
+            </colgroup>
+            <thead>
+              <tr>
+                <th>구분</th>
+                <th>기간</th>
+                <th>신청일</th>
+                <th>상태</th>
+              </tr>
+            </thead>
+            <tbody>
+              <template v-if="hasVacations">
+                <tr v-for="(item, idx) in vacationList" :key="`${item.vcatnId}-${idx}`" @click="handleItemClick(item.vcatnId)">
+                  <td>{{ item.vcatnKndNm }}</td>
+                  <td>{{ $formattedDates(item) }}</td>
+                  <td>{{ item.rgsde }}</td>
+                  <td>{{ item.confmAtNm }}</td>
+                </tr>
+              </template>
+              <tr v-else>
+                <td colspan="4">해당 기간 내 등록된 휴가가 없습니다.</td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+        <Pagination :search="searchParams" @onChange="handlePageChange" />
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+// API
+import { findVcatnSummaryProc, findVcatnsProc } from '../../../../resources/api/vcatn';
+
+export default {
+  name: 'VacationList',
+
+  data() {
+    return {
+      searchParams: {
+        bgnde: null,
+        endde: null,
+        vcatnKnd: null,
+        currentUserId: this.$store.state.userInfo.userId,
+      },
+      vacationList: [],
+      totalDays: 0,
+      carriedOverDays: 0,
+      usedLeaveItem: {},
+      remainingDays: 0,
+      otherItems: [],
+    };
+  },
+
+  computed: {
+    // 휴가 목록 유무
+    hasVacations() {
+      return this.vacationList.length > 0;
+    },
+  },
+
+  async mounted() {
+    await this.fetchVacationSummary(); // 휴가 현황 조회
+    await this.fetchVacationList(); // 휴가 목록 조회
+  },
+
+  methods: {
+    // 휴가 현황 조회
+    async fetchVacationSummary() {
+      try {
+        const response = await findVcatnSummaryProc(this.searchParams);
+        const result = response.data.data;
+
+        this.searchParams.bgnde = result.userYrycVO.bgnde;
+        this.searchParams.endde = new Date().toISOString().split('T')[0];
+
+        this.totalDays = result.totalDays;
+        this.carriedOverDays = result.carriedOverDays;
+        this.usedLeaveItem = result.usedLeaveItem;
+        this.remainingDays = result.remainingDays;
+        this.otherItems = result.otherItems;
+      } catch (error) {
+        this.handleError(error);
+      }
+    },
+
+    // 휴가 목록 조회
+    async fetchVacationList() {
+      try {
+        delete this.searchParams.vcatnKndList;
+
+        const response = await findVcatnsProc(this.searchParams);
+        const result = response.data.data;
+
+        this.vacationList = result.lists;
+        this.searchParams = result.search;
+      } catch (error) {
+        this.handleError(error);
+      }
+    },
+
+    // 검색 핸들러 (검색 시 현재 페이지를 1로 변경 후 조회)
+    async handleSearchChange() {
+      if (!this.searchParams.bgnde || !this.searchParams.endde) {
+        return;
+      }
+
+      const startDate = new Date(this.searchParams.bgnde);
+      const endDate = new Date(this.searchParams.endde);
+      if (endDate < startDate) {
+        alert('종료일은 시작일보다 이전일 수 없습니다.');
+        this.searchParams.endde = this.searchParams.bgnde;
+        return;
+      }
+
+      await this.handlePageChange(1);
+    },
+
+    // 필터 변경 핸들러
+    async handleFilterChange(vcatnKnd) {
+      this.searchParams.vcatnKnd = vcatnKnd;
+      await this.handlePageChange(1);
+    },
+
+    // 페이지 변경 핸들러
+    async handlePageChange(currentPage) {
+      this.searchParams.currentPage = Number(currentPage);
+      await this.$nextTick();
+      await this.fetchVacationList();
+    },
+
+    // 상세 페이지 이동 핸들러
+    handleItemClick(id) {
+      this.handleNavigation('view', id);
+    },
+
+    // 페이지 이동 핸들러
+    handleNavigation(type, id) {
+      const routeMap = {
+        'list': { name: 'VcatnListPage' },
+        'view': { name: 'VcatnViewPage', query: { id } },
+        'insert': { name: 'VcatnInsertPage', query: this.$isEmpty(id) ? {} : { id } },
+        'reapply': { name: 'VcatnInsertPage', query: { id, type: 'reapply' } },
+      };
+
+      const route = routeMap[type];
+      if (route) {
+        this.$router.push(route);
+      } else {
+        alert("올바르지 않은 경로입니다.");
+        this.$router.push(routeMap['list']);
+      }
+    },
+
+    // 에러 핸들러
+    handleError(error) {
+      const message = error.response?.data?.message || "에러가 발생했습니다.";
+      alert(message);
+    },
+  },
+};
+</script>
+<style scoped>
+tr {
+  cursor: pointer;
+}
+
+/* 미사용만 커서 미적용 (필터 조회 안됨) */
+.box:not(:nth-child(3)) {
+  cursor: pointer;
+}
+
+.box:nth-child(2) {
+  color: #1D75E1 !important;
+}
+
+.box:nth-child(3) {
+  color: #E92727 !important;
+}
+
+.box:nth-child(4) {
+  color: #3C97AB !important;
+}
+
+.box:nth-child(5) {
+  color: #A36CD4 !important;
+}
+
+.box:nth-child(6) {
+  color: #F7941C !important;
+}
+</style>(파일 끝에 줄바꿈 문자 없음)
 
client/views/pages/Manager/attendance/VcatnView.vue (added)
+++ client/views/pages/Manager/attendance/VcatnView.vue
@@ -0,0 +1,257 @@
+<template>
+  <div class="card">
+    <div class="card-body">
+      <h2 class="card-title">휴가 현황</h2>
+      <div class="form-card">
+        <h1>휴가신청서</h1>
+        <SanctnViewList v-if="vacationInfo.sanctnList.length > 0" :sanctns="vacationInfo.sanctnList" />
+        <div class="tbl-wrap">
+          <table class="tbl data">
+            <tbody>
+              <tr>
+                <th>유형</th>
+                <td>{{ vacationInfo.vcatnKndNm }}</td>
+                <th>신청자</th>
+                <td>{{ vacationInfo.userNm }}</td>
+              </tr>
+              <tr>
+                <th>부서</th>
+                <td>{{ vacationInfo.deptNm }}</td>
+                <th>직급</th>
+                <td>{{ vacationInfo.clsfNm }}</td>
+              </tr>
+              <tr>
+                <th>기간</th>
+                <td colspan="3">{{ $formattedDates(vacationInfo) }}</td>
+              </tr>
+              <tr>
+                <th>세부사항</th>
+                <td colspan="3">
+                  <ViewerComponent :content="vacationInfo.detailCn" />
+                </td>
+              </tr>
+              <tr>
+                <th>신청일</th>
+                <td colspan="3">{{ vacationInfo.rgsde }}</td>
+              </tr>
+              <tr>
+                <th>상태</th>
+                <td colspan="3">{{ vacationInfo.confmAtNm }}</td>
+              </tr>
+              <tr v-if="approvalStatus === 'rejected'">
+                <th>반려사유</th>
+                <td colspan="3">{{ getRejectionReason(vacationInfo.sanctnList) }}</td>
+              </tr>
+              <!-- 붉은 색 등 하여간 눈에 띄게 표기해야 함!! -->
+              <tr>
+                <td colspan="4">금년도 연차 소진으로 내년도 연차를 선 사용하는 휴가 신청건입니다.</td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+      <div class="buttons">
+        <template v-if="isConsultationApprover">
+          <button type="button" class="btn sm primary" @click="handleApproval('A')">승인</button>
+          <button type="button" class="btn sm btn-red" @click="showConsultationPopup = true">반려</button>
+        </template>
+        <template v-else-if="isApplicant">
+          <template v-if="approvalStatus === 'waiting'">
+            <button type="button" class="btn sm btn-red" @click="handleDelete">신청취소</button>
+            <button type="button" class="btn sm secondary" @click="handleNavigation('insert', pageId)">수정</button>
+          </template>
+          <template v-if="approvalStatus === 'rejected'">
+            <button type="button" class="btn sm secondary" @click="handleNavigation('reapply', pageId)">재신청</button>
+          </template>
+        </template>
+        <button type="button" class="btn sm tertiary" @click="handleNavigation('list')">목록</button>
+      </div>
+    </div>
+  </div>
+  <ReturnPopup v-if="showConsultationPopup" @close="showConsultationPopup = false" @confirm="handleRejection" />
+</template>
+<script>
+import ReturnPopup from '../../../component/Popup/ReturnPopup.vue';
+import ViewerComponent from '../../../component/editor/ViewerComponent.vue';
+import SanctnViewList from '../../../component/Sanctn/SanctnViewList.vue';
+// API
+import { findVcatnProc, deleteVcatnProc } from '../../../../resources/api/vcatn';
+import { updateConfmAtProc } from '../../../../resources/api/sanctns';
+
+export default {
+  name: 'VacationView',
+
+  components: {
+    ReturnPopup,
+    ViewerComponent,
+    SanctnViewList,
+  },
+
+  data() {
+    return {
+      pageId: null,
+      pageMode: null,
+      showConsultationPopup: false,
+      vacationInfo: {
+        userId: null,
+        userNm: null,
+        vcatnKndNm: null,
+        deptNm: null,
+        clsfNm: null,
+        bgnde: null,
+        beginHour: null,
+        beginMnt: null,
+        endde: null,
+        endHour: null,
+        endMnt: null,
+        detailCn: null,
+        confmAtNm: null,
+        rgsde: null,
+        sanctnList: []
+      },
+      returnResn: null,
+    };
+  },
+
+  computed: {
+    // 결재 상태
+    approvalStatus() {
+      const sanctnList = this.vacationInfo.sanctnList;
+      if (sanctnList.length === 0) return 'none';
+
+      // 하나라도 반려된 경우 > 반려
+      if (sanctnList.some(item => item.confmAt === 'R')) {
+        return 'rejected';
+      }
+
+      // 전부 승인된 경우 > 승인
+      if (sanctnList.every(item => item.confmAt === 'A')) {
+        return 'approved';
+      }
+
+      // 그 외 > 대기
+      return 'waiting';
+    },
+
+    // 작성자 여부
+    isApplicant() {
+      return this.vacationInfo.userId === this.$store.state.userInfo.userId;
+    },
+
+    // 결재자 여부
+    isConsultationApprover() {
+      const sanctnList = this.vacationInfo.sanctnList;
+      const mySanctn = sanctnList.find(
+        item => item.confmerId == this.$store.state.userInfo.userId
+      );
+      return mySanctn && mySanctn.confmAt === 'W' && this.pageMode === 'sanctns';
+    },
+  },
+
+  async created() {
+    this.pageId = this.$route.query.id;
+    this.pageMode = this.$route.query.type;
+    if (this.$isEmpty(this.pageId)) {
+      alert("게시물이 존재하지 않습니다.");
+      this.handleNavigation('list');
+    }
+  },
+
+  mounted() {
+    this.fetchData(); // 휴가 정보 조회
+  },
+
+  methods: {
+    // 휴가 정보 조회
+    async fetchData() {
+      try {
+        const response = await findVcatnProc(this.pageId);
+        const result = response.data.data;
+
+        this.vacationInfo = result;
+      } catch (error) {
+        this.handleError(error);
+        this.handleNavigation('list');
+      }
+    },
+
+    // 결재 승인/반려 처리
+    async handleApproval(value) {
+      try {
+        const sanctnList = this.vacationInfo.sanctnList;
+        const sanctn = sanctnList.find(item => item.confmerId == this.$store.state.userInfo.userId);
+
+        if (!sanctn) {
+          alert('결재 권한이 없습니다.');
+          return;
+        }
+
+        const data = {
+          sanctnOrdr: sanctn.sanctnOrdr,
+          sanctnIem: sanctn.sanctnIem,
+          sanctnMbyId: this.pageId,
+          confmAt: value,
+          returnResn: this.returnResn,
+        };
+
+        await updateConfmAtProc(sanctn.sanctnId, data);
+        alert("승인했습니다.");
+        this.fetchData();
+      } catch (error) {
+        this.handleError(error);
+      }
+    },
+
+    // 반려 결재 핸들러
+    async handleRejection(reason) {
+      this.returnResn = reason;
+      await this.handleApproval('R');
+      this.showConsultationPopup = false;
+    },
+
+    // 휴가 취소 (완전 삭제)
+    async handleDelete() {
+      const isCheck = confirm("휴가 신청을 취소하시겠습니까?");
+      if (!isCheck) return;
+
+      try {
+        await deleteVcatnProc(this.pageId);
+        alert("휴가 신청이 취소되었습니다.");
+        this.handleNavigation('list');
+      } catch (error) {
+        this.handleError(error);
+      }
+    },
+
+    // 페이지 이동 핸들러
+    handleNavigation(type, id) {
+      const routeMap = {
+        'list': { name: this.pageMode === 'sanctns' ? 'PendingApprovalListPage' : 'VcatnListPage' },
+        'view': { name: 'VcatnViewPage', query: { id } },
+        'insert': { name: 'VcatnInsertPage', query: this.$isEmpty(id) ? {} : { id } },
+        'reapply': { name: 'VcatnInsertPage', query: { id, type: 'reapply' } },
+      };
+
+      const route = routeMap[type];
+      if (route) {
+        this.$router.push(route);
+      } else {
+        alert("올바르지 않은 경로입니다.");
+        this.$router.push(routeMap['list']);
+      }
+    },
+
+    // 에러 핸들러
+    handleError(error) {
+      const message = error.response?.data?.message || '에러가 발생했습니다.';
+      alert(message);
+    },
+
+    // 반려 사유 유틸리티
+    getRejectionReason(sanctnList) {
+      const rejectedItem = sanctnList.find(item => item.confmAt === 'R');
+      return rejectedItem?.returnResn || '-';
+    },
+  }
+};
+</script>(파일 끝에 줄바꿈 문자 없음)
client/views/pages/Manager/attendance/attendance.vue
--- client/views/pages/Manager/attendance/attendance.vue
+++ client/views/pages/Manager/attendance/attendance.vue
@@ -44,7 +44,7 @@
         </summary>
         <ul>
           <li>
-            <router-link :to="{ name: 'hyugaStatue' }" exact-active-class="active-link" v-slot="{ isExactActive }">
+            <router-link :to="{ name: 'VcatnList' }" exact-active-class="active-link" v-slot="{ isExactActive }">
               <p>휴가 현황</p>
               <div class="icon" v-if="isExactActive">
                 <img :src="menuicon" alt="">
@@ -52,7 +52,7 @@
             </router-link>
           </li>
           <li>
-            <router-link :to="{ name: 'hyugaInsert' }" exact-active-class="active-link" v-slot="{ isExactActive }">
+            <router-link :to="{ name: 'VcatnInsert' }" exact-active-class="active-link" v-slot="{ isExactActive }">
               <p>휴가 신청</p>
               <div class="icon" v-if="isExactActive">
                 <img :src="menuicon" alt="">
client/views/pages/Manager/attendance/buseoAttendance.vue
--- client/views/pages/Manager/attendance/buseoAttendance.vue
+++ client/views/pages/Manager/attendance/buseoAttendance.vue
@@ -28,7 +28,7 @@
             </div>
         </div>
       </div>
-     
+
       <!-- Table  -->
       <div class="tbl-wrap">
         <table id="myTable" class="tbl data buseo">
@@ -67,40 +67,40 @@
   </tr>
           </tbody>
         </table>
-  
+
       </div>
       <div class="pagination">
         <ul>
       <!-- 왼쪽 화살표 (이전 페이지) -->
-      <li 
-        class="arrow" 
-        :class="{ disabled: currentPage === 1 }" 
+      <li
+        class="arrow"
+        :class="{ disabled: currentPage === 1 }"
         @click="changePage(currentPage - 1)"
       >
       &lt;
       </li>
 
       <!-- 페이지 번호 -->
-      <li 
-        v-for="page in totalPages" 
-        :key="page" 
-        :class="{ active: currentPage === page }" 
+      <li
+        v-for="page in totalPages"
+        :key="page"
+        :class="{ active: currentPage === page }"
         @click="changePage(page)"
       >
         {{ page }}
       </li>
 
       <!-- 오른쪽 화살표 (다음 페이지) -->
-      <li 
-        class="arrow" 
-        :class="{ disabled: currentPage === totalPages }" 
+      <li
+        class="arrow"
+        :class="{ disabled: currentPage === totalPages }"
         @click="changePage(currentPage + 1)"
       >
       &gt;
       </li>
     </ul>
   </div>
-      
+
     </div>
   </div>
 </div>
@@ -190,7 +190,7 @@
     },
     goToPage(type) {
       if (type === '휴가') {
-  this.$router.push({ name: 'HyugaDetail' });
+  this.$router.push({ name: 'VcatnView' });
 } else if (type === '출장') {
   this.$router.push({ name: 'ChuljangDetail' });
 }
 
client/views/pages/Manager/attendance/hyugaStatue.vue (deleted)
--- client/views/pages/Manager/attendance/hyugaStatue.vue
@@ -1,269 +0,0 @@
-<template>
-  <div class="col-lg-12">
-    <div class="card">
-      <div class="card-body">
-        <h2 class="card-title">휴가 현황</h2>
-        <div class="form-group">
-          <div class="form-conts">
-            <div class="form-conts datepicker-conts">
-              <div class="datepicker-input">
-                <input type="date" class="form-control datepicker cal" v-model="request.bgnde" style="max-width: 200px;" @change="changeDate" />
-                <mark>~</mark>
-                <input type="date" class="form-control datepicker cal" v-model="request.endde" style="max-width: 200px;" @change="changeDate" />
-              </div>
-            </div>
-          </div>
-        </div>
-        <div class="boxs">
-          <div class="color-boxs">
-            <div v-for="(item, idx) of useSummary" :key="idx" :class="getBoxClass(idx)" @click="fnFindUseList(item)">
-              <h3>{{ item.name }}</h3> {{ item.value }}
-            </div>
-            <div v-for="(item, idx) of notUseSummary" :key="idx" :class="getBoxClass(useSummary.length + idx)" @click="fnFindNotUseList(item.code)">
-              <h3>{{ item.codeNm }}</h3> {{ item.value }}
-            </div>
-          </div>
-        </div>
-        <div class="tbl-wrap">
-          <table id="myTable" class="tbl data">
-            <colgroup>
-              <col style="width: 15%;">
-              <col style="width: 40%;">
-              <col style="width: 30%;">
-              <col style="width: 15%;">
-            </colgroup>
-            <thead>
-              <tr>
-                <th>구분</th>
-                <th>기간</th>
-                <th>신청일</th>
-                <th>상태</th>
-              </tr>
-            </thead>
-            <tbody>
-              <template v-if="lists.length > 0">
-                <tr v-for="(item, idx) in lists" :key="idx" @click="fnMoveTo('view', item.vcatnId)">
-                  <td>{{ item.vcatnKndNm }}</td>
-                  <td>{{ $formattedDates(item) }}</td>
-                  <td>{{ item.rgsde }}</td>
-                  <td>{{ item.confmAtNm }}</td>
-                </tr>
-              </template>
-              <tr v-else>
-                <td colspan="4">해당 기간 내 등록된 휴가가 없습니다.</td>
-              </tr>
-            </tbody>
-          </table>
-        </div>
-        <Pagination :search="request" @onChange="fnChangeCurrentPage" />
-      </div>
-    </div>
-  </div>
-</template>
-<script>
-import { SearchOutlined } from '@ant-design/icons-vue';
-import Pagination from '../../../component/Pagination.vue';
-// API
-import { findVcatnsSummary, findVcatnsProc } from '../../../../resources/api/vcatn';
-
-export default {
-  components: {
-    SearchOutlined,
-    Pagination
-  },
-
-  data() {
-    return {
-      photoicon: "/client/resources/img/photo_icon.png",
-
-      color: ['blue', 'red', 'green', 'purple', 'orange'],
-      colorMap: [],
-
-      useSummary: [], // 연차 소모성 휴가
-      notUseSummary: [], // 연차 비 소모성 휴가
-
-      request: {
-        vcatnKnds: null, // 휴가 종류
-        bgnde: null, // 시작일
-        endde: null, // 종료일
-      },
-      lists: [],
-    };
-  },
-
-  computed: {},
-
-  created() { },
-
-  mounted() {
-    this.findSummary(); // 현황 조회
-    this.findList(); // 목록 조회
-  },
-
-  methods: {
-    // 현황 조회
-    async findSummary() {
-      const vm = this;
-      try {
-        const response = await findVcatnsSummary(vm.request);
-        const result = response.data.data;
-
-        this.request.bgnde = new Date(result.data.bgnde).toISOString().split('T')[0];
-        this.request.endde = new Date(result.data.endde).toISOString().split('T')[0];
-        if (result.data.yryc < 1) {
-          this.request.endde = new Date().toISOString().split('T')[0];
-        }
-
-        let yrycCyfdCo = result.data.yrycCyfdCo;
-        let totalCo = result.data.yrycCo + yrycCyfdCo;
-        let useCo = result.useSummary.useCo;
-        let notUseCo = totalCo - useCo;
-
-        this.useSummary = []; // 초기화
-        this.useSummary.push({ key: 'totalCo', name: '전체', value: totalCo + '(' + yrycCyfdCo + ')' });
-        this.useSummary.push({ key: 'useCo', name: '사용', value: useCo, codeList: result.useSummary.codeList });
-        this.useSummary.push({ key: 'notUseCo', name: '미사용', value: notUseCo });
-
-        this.notUseSummary = []; // 초기화
-        this.notUseSummary = result.notUseSummary;
-
-        this.colorMap = []; // 초기화
-        const keys = Object.keys(this.notUseSummary);
-        for (let i = 0; i < Object.keys(this.notUseSummary).length; i++) {
-          this.colorMap.push({
-            code: keys[i].code,
-            class: this.color[this.useSummary.length + i],
-          })
-        }
-      } catch (error) {
-        if (error.response) {
-          alert(error.response.data.message);
-        } else {
-          alert("에러가 발생했습니다.");
-        }
-        console.error(error.message);
-      };
-    },
-
-    // 목록 조회
-    async findList() {
-      const vm = this;
-      try {
-        delete vm.request.vcatnKndList;
-
-        const response = await findVcatnsProc(vm.request);
-        const result = response.data.data;
-
-        vm.lists = result.lists;
-        vm.request = result.search;
-      } catch (error) {
-        if (error.response) {
-          alert(error.response.data.message);
-        } else {
-          alert("에러가 발생했습니다.");
-        }
-        console.error(error.message);
-      };
-    },
-
-    // 현황에 클래스 부여
-    getBoxClass(idx) {
-      if (idx === 0) return 'box';
-      return `box ${this.color[(idx - 1) % this.color.length]}`;
-    },
-
-    // 날짜 선택
-    changeDate() {
-      if (!this.request.bgnde || !this.request.endde) {
-        return;
-      }
-
-      const startDate = new Date(this.request.bgnde);
-      const endDate = new Date(this.request.endde);
-      if (endDate < startDate) {
-        alert('종료일은 시작일보다 이전일 수 없습니다.');
-        this.request.endde = this.request.bgnde;
-        return;
-      }
-
-      this.findList(); // 목록 조회
-    },
-
-    // 목록 필터 조회
-    fnFindUseList(item) {
-      if (item.key === 'totalCo') {
-        this.request.vcatnKnds = null;
-      } else if (item.key === 'useCo') {
-        this.request.vcatnKnds = item.codeList.map(code => code.code).join(", ");
-      } else {
-        return;
-      }
-
-      this.findList(); // 목록 조회
-    },
-
-    // 목록 필터 조회
-    fnFindNotUseList(vcatnKnd) {
-      this.request.vcatnKnds = vcatnKnd;
-
-      this.findList(); // 목록 조회
-    },
-
-    // 페이지 이동
-    fnChangeCurrentPage(currentPage) {
-      this.request.currentPage = Number(currentPage);
-      this.$nextTick(() => {
-        this.findList();
-      });
-    },
-
-    // 페이지 이동
-    fnMoveTo(type, id) {
-      const routes = {
-        'list': { name: 'hyugaStatue' },
-        'view': { name: 'HyugaDetail', query: { id } },
-        'edit': { name: 'hyugaInsert', query: this.$isEmpty(id) ? {} : { id } },
-      };
-
-      if (routes[type]) {
-        if (!this.$isEmpty(this.pageId) && type === 'list') {
-          this.$router.push({ name: 'HyugaDetail', query: { id: this.pageId } });
-        }
-        this.$router.push(routes[type]);
-      } else {
-        alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다.");
-        this.$router.push(routes['list']);
-      }
-    },
-  },
-};
-</script>
-<style scoped>
-tr {
-  cursor: pointer;
-}
-
-.box {
-  cursor: pointer;
-}
-
-.vcatnKnd.red {
-  color: #E92727 !important;
-}
-
-.vcatnKnd.green {
-  color: #3C97AB !important;
-}
-
-.vcatnKnd.blue {
-  color: #1D75E1 !important;
-}
-
-.vcatnKnd.purple {
-  color: #A36CD4 !important;
-}
-
-.vcatnKnd.orange {
-  color: #F7941C !important;
-}
-</style>(파일 끝에 줄바꿈 문자 없음)
client/views/pages/Manager/financial/ChuljangCostList.vue
--- client/views/pages/Manager/financial/ChuljangCostList.vue
+++ client/views/pages/Manager/financial/ChuljangCostList.vue
@@ -129,9 +129,9 @@
   methods: {
     goToDetailPage(item) {
     // item.id 또는 다른 식별자를 사용하여 URL을 구성할 수 있습니다.
-    this.$router.push({ 
+    this.$router.push({
       name: 'employeeSalaryDetail',
-       query: { id: item.id } 
+       query: { id: item.id }
       });
   },
     formatCurrency(amount) {
@@ -160,7 +160,7 @@
     },
     goToPage(type) {
       if (type === '휴가') {
-        this.$router.push({ name: 'HyugaInsert' });
+        this.$router.push({ name: 'VcatnInsert' });
       } else if (type === '출장') {
         this.$router.push({ name: 'ChuljangDetail' });
       }
client/views/pages/Manager/financial/MeetingCostList.vue
--- client/views/pages/Manager/financial/MeetingCostList.vue
+++ client/views/pages/Manager/financial/MeetingCostList.vue
@@ -130,7 +130,7 @@
     },
     goToPage(type) {
       if (type === '휴가') {
-        this.$router.push({ name: 'HyugaInsert' });
+        this.$router.push({ name: 'VcatnInsert' });
       } else if (type === '출장') {
         this.$router.push({ name: 'ChuljangDetail' });
       }
client/views/pages/Manager/financial/employeeSalaryList.vue
--- client/views/pages/Manager/financial/employeeSalaryList.vue
+++ client/views/pages/Manager/financial/employeeSalaryList.vue
@@ -154,7 +154,7 @@
     },
     goToPage(type) {
       if (type === '휴가') {
-        this.$router.push({ name: 'HyugaInsert' });
+        this.$router.push({ name: 'VcatnInsert' });
       } else if (type === '출장') {
         this.$router.push({ name: 'ChuljangDetail' });
       }
client/views/pages/Manager/financial/salaryList.vue
--- client/views/pages/Manager/financial/salaryList.vue
+++ client/views/pages/Manager/financial/salaryList.vue
@@ -143,7 +143,7 @@
     },
     goToPage(type) {
       if (type === '명세서') {
-        this.$router.push({ name: 'HyugaInsert' });
+        this.$router.push({ name: 'VcatnInsert' });
       } else if (type === '출장') {
         this.$router.push({ name: 'ChuljangDetail' });
       }
client/views/pages/Manager/sanctn/MyApprovalRequestList.vue
--- client/views/pages/Manager/sanctn/MyApprovalRequestList.vue
+++ client/views/pages/Manager/sanctn/MyApprovalRequestList.vue
@@ -156,7 +156,7 @@
       const approvalType = id.split('_')[0];
 
       if (approvalType === "VCATN") {
-        this.$router.push({ name: 'HyugaDetail', query: { id } });
+        this.$router.push({ name: 'VcatnView', query: { id } });
       } if (approvalType === "BSRP") {
         this.$router.push({ name: 'ChuljangDetailAll', query: { id } });
       }
@@ -165,7 +165,7 @@
     // 등록 페이지 이동
     handleRegistrationNavigation(type) {
       if (type === "휴가") {
-        this.$router.push({ name: 'hyugaInsert' });
+        this.$router.push({ name: 'VcatnInsert' });
       } else if (type === "출장") {
         this.$router.push({ name: 'ChuljangInsert' });
       }
client/views/pages/Manager/sanctn/PendingApprovalList.vue
--- client/views/pages/Manager/sanctn/PendingApprovalList.vue
+++ client/views/pages/Manager/sanctn/PendingApprovalList.vue
@@ -40,12 +40,17 @@
                 </tr>
               </thead>
               <tbody>
-                <tr v-for="(item, idx) in pendingApprovalList" :key="idx" @click="handleDetailNavigation(item.sanctnMbyId)">
-                  <td>{{ item.sanctnIemNm }}</td>
-                  <td>{{ item.sanctnSeNm }}</td>
-                  <td>{{ item.registerNm }}</td>
-                  <td>{{ $formattedDates(item) }}</td>
-                  <td>{{ item.rgsde }}</td>
+                <template v-if="pendingApprovalList.length > 0">
+                  <tr v-for="(item, idx) in pendingApprovalList" :key="idx" @click="handleDetailNavigation(item.sanctnMbyId)">
+                    <td>{{ item.sanctnIemNm }}</td>
+                    <td>{{ item.sanctnSeNm }}</td>
+                    <td>{{ item.registerNm }}</td>
+                    <td>{{ $formattedDates(item) }}</td>
+                    <td>{{ item.rgsde }}</td>
+                  </tr>
+                </template>
+                <tr v-else>
+                  <td colspan="6">게시물이 존재하지 않습니다.</td>
                 </tr>
               </tbody>
             </table>
@@ -96,13 +101,18 @@
                 </tr>
               </thead>
               <tbody>
-                <tr v-for="(item, index) in approvalHistoryList" :key="index" class="expired" @click="handleDetailNavigation(item.sanctnMbyId)">
-                  <td>{{ item.sanctnIemNm }}</td>
-                  <td>{{ item.sanctnSeNm }}</td>
-                  <td>{{ item.registerNm }}</td>
-                  <td>{{ $formattedDates(item) }}</td>
-                  <td>{{ item.rgsde }}</td>
-                  <td>{{ item.confmAtNm }}</td>
+                <template v-if="approvalHistoryList.length > 0">
+                  <tr v-for="(item, index) in approvalHistoryList" :key="index" class="expired" @click="handleDetailNavigation(item.sanctnMbyId)">
+                    <td>{{ item.sanctnIemNm }}</td>
+                    <td>{{ item.sanctnSeNm }}</td>
+                    <td>{{ item.registerNm }}</td>
+                    <td>{{ $formattedDates(item) }}</td>
+                    <td>{{ item.rgsde }}</td>
+                    <td>{{ item.confmAtNm }}</td>
+                  </tr>
+                </template>
+                <tr v-else>
+                  <td colspan="6">게시물이 존재하지 않습니다.</td>
                 </tr>
               </tbody>
             </table>
@@ -129,8 +139,8 @@
     return {
       sectionIcon: "/client/resources/img/h3icon.png",
 
-      yearOptions: [2023, 2024, 2025], // 연도 목록
-      monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], // 월 목록
+      yearOptions: [],
+      monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
 
       // 승인 대기 목록
       pendingApprovalList: [],
@@ -140,6 +150,7 @@
         isConfmAt: true, // 변경불가 (고정값)
         searchText: '',
         currentUserId: this.$store.state.userInfo.userId, // 변경불가 (고정값)
+        recordSize: 5,
       },
       // 승인 이력 목록
       approvalHistoryList: [],
@@ -149,6 +160,7 @@
         isConfmAt: false, // 변경불가 (고정값)
         searchText: '',
         currentUserId: this.$store.state.userInfo.userId, // 변경불가 (고정값)
+        recordSize: 5,
       },
     };
   },
@@ -227,7 +239,7 @@
       const approvalType = id.split('_')[0];
 
       if (approvalType === "VCATN") {
-        this.$router.push({ name: 'HyugaDetail', query: { id, type: 'sanctns' } });
+        this.$router.push({ name: 'VcatnViewPage', query: { id, type: 'sanctns' } });
       } if (approvalType === "BSRP") {
         this.$router.push({ name: 'BsrpViewPage', query: { id, type: 'sanctns' } });
       }
client/views/pages/Manager/task/projectTuib.vue
--- client/views/pages/Manager/task/projectTuib.vue
+++ client/views/pages/Manager/task/projectTuib.vue
@@ -26,7 +26,7 @@
             </div>
         </div>
       </div>
-     
+
       <!-- Table  -->
       <div class="tbl-wrap">
         <table id="myTable" class="tbl data buseo">
@@ -58,40 +58,40 @@
   </tr>
           </tbody>
         </table>
-  
+
       </div>
       <div class="pagination">
         <ul>
       <!-- 왼쪽 화살표 (이전 페이지) -->
-      <li 
-        class="arrow" 
-        :class="{ disabled: currentPage === 1 }" 
+      <li
+        class="arrow"
+        :class="{ disabled: currentPage === 1 }"
         @click="changePage(currentPage - 1)"
       >
       &lt;
       </li>
 
       <!-- 페이지 번호 -->
-      <li 
-        v-for="page in totalPages" 
-        :key="page" 
-        :class="{ active: currentPage === page }" 
+      <li
+        v-for="page in totalPages"
+        :key="page"
+        :class="{ active: currentPage === page }"
         @click="changePage(page)"
       >
         {{ page }}
       </li>
 
       <!-- 오른쪽 화살표 (다음 페이지) -->
-      <li 
-        class="arrow" 
-        :class="{ disabled: currentPage === totalPages }" 
+      <li
+        class="arrow"
+        :class="{ disabled: currentPage === totalPages }"
         @click="changePage(currentPage + 1)"
       >
       &gt;
       </li>
     </ul>
   </div>
-      
+
     </div>
   </div>
 </div>
@@ -164,7 +164,7 @@
     },
     goToPage(type) {
   if (type === '휴가') {
-    this.$router.push({ name: 'HyugaDetail' });
+    this.$router.push({ name: 'VcatnView' });
   } else if (type === '출장') {
     this.$router.push({ name: 'ChuljangDetail' });
   }
Add a comment
List