
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
<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>
<tr class="red" v-if="excessReqst">
<td colspan="2">금년도 연차 소진으로 인해 내년도 연차를 선 사용하는 휴가 신청건입니다.</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';
import { findUserExcessReqstAtProc } from '../../../../resources/api/userYryc';
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: [],
excessReqst: false,
};
},
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.fetchUserExcessReqstAt(); // 사용자 연차 초과 신청 여부 조회
await this.fetchVacationTypes(); // 휴가 유형 조회
this.approvalCodes = await this.$findChildCodes('sanctn_code'); // 결재 구분 조회
},
mounted() {
if (!this.$isEmpty(this.pageId)) {
this.fetchData(); // 상세 조회
}
},
methods: {
// 사용자 연차 초과 신청 여부 조회
async fetchUserExcessReqstAt() {
try {
const response = await findUserExcessReqstAtProc();
const result = response.data.data;
this.excessReqst = result.isNegativeAnnualLeave;
if (this.excessReqst) {
const chk = confirm("금년도 연차를 모두 소진하였습니다.\n내년도 연차를 미리 사용하시겠습니까?");
if (!chk) {
this.handleNavigation('list');
}
} else {
if (result.isTotalCntOver) {
alert("이미 금년도 연차 보유 수 만큼 휴가를 신청한 상태입니다.\n마이너스 이월은 기존 휴가 신청이 모두 승인된 뒤 신청 가능합니다.");
this.handleNavigation('list');
}
}
} catch (error) {
this.handleError(error);
this.handleNavigation('list');
}
},
// 휴가 유형 조회
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,
excessReqstAt: this.excessReqst ? 'Y' : 'N',
};
},
},
};
</script>
<style scoped>
.red th,
.red td {
text-align: center;
color: #fb2c36 !important;
background-color: #fef2f2 !important;
}
</style>