
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"><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>