
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="row g-3 needs-validation">
<div class="col-12">
<label for="inputName5" class="form-label">
<p>유형<span class="require"><img :src="require" alt=""></span></p>
</label>
<select class="form-select" style="max-width: 200px;" v-model="editData.vcatnKnd" @change="fnOnchangeVcatnKnd">
<option value="" disabled hidden>유형 선택</option>
<option v-for="(item, idx) of cmmnCodes.vcatnKndCodeList" :key="idx" :value="item.code">{{ item.codeNm }}</option>
</select>
<template v-if="showSubTypeSelect">
<select class="form-select mt-2" style="max-width: 200px;" v-model="editData.vcatnSubKnd" @change="fnOnchangeVcatnSubKnd">
<option value="" disabled hidden>세부 유형 선택</option>
<option v-for="(item, idx) of subTypes" :key="idx" :value="item.code">{{ item.codeNm }}</option>
</select>
</template>
</div>
<div class="col-12">
<label for="bgnde" class="form-label">
<p>시작일<span class="require"><img :src="require" alt=""></span></p>
</label>
<div class="d-flex gap-1">
<input type="date" class="form-control" id="bgnde" v-model="editData.bgnde" @keydown="preventKeyboard" />
<input type="text" class="form-control" placeholder="시" style="width: 100px;" v-model="editData.beginHour" readonly />
<input type="text" class="form-control" placeholder="분" style="width: 100px;" v-model="editData.beginMnt" readonly />
</div>
</div>
<div class="col-12">
<label for="endde" class="form-label">
<p>종료일<span class="require"><img :src="require" alt=""></span></p>
</label>
<div class="d-flex gap-1">
<input type="date" class="form-control" id="endde" v-model="editData.endde" :readonly="dayCnt === 0.5" @keydown="preventKeyboard" />
<input type="text" class="form-control" placeholder="시" style="width: 100px;" v-model="editData.endHour" readonly />
<input type="text" class="form-control" placeholder="분" style="width: 100px;" v-model="editData.endMnt" readonly />
</div>
</div>
<div class="col-12">
<label for="totalDays" class="form-label">사용 휴가일</label>
<input type="text" class="form-control" id="totalDays" v-model="totalDays" readonly />
</div>
<div class="col-12">
<label for="member" class="form-label">
<span>승인자<span class="require"><img :src="require" alt=""></span></span>
<button type="button" title="추가" @click="isOpenModal = true">
<PlusCircleFilled />
</button>
</label>
<HrPopup v-if="isOpenModal && isCodesLoaded" :sanctns="editData.sanctnList" :cmmnCodes="cmmnCodes" @onSelected="fnAddSanctn" @close="isOpenModal = false" />
<div class="approval-container">
<SanctnList v-model:sanctns="editData.sanctnList" @delSanctn="fnDelSanctn" />
</div>
</div>
<div class="col-12 border-x hyuga">
<label for="prvonsh" class="form-label">세부사항</label>
<div>
<EditorComponent v-model:contents="editData.detailCn" />
</div>
</div>
</div>
<div class="buttons">
<button type="button" class="btn btn-red" @click="fnRecord">이전 승인자 불러오기</button>
<button type="button" class="btn primary" @click="fnSave">신청</button>
<button type="button" class="btn 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: '',
vcatnSubKnd: '', // 세부 유형 추가
deptId: null,
clsf: null,
bgnde: null,
beginHour: null,
beginMnt: null,
endde: null,
endHour: null,
endMnt: null,
detailCn: null,
confmAt: null,
sanctnList: [],
},
dayCnt: 0,
totalDays: 0,
cmmnCodes: {},
workConfig: {
startHour: 9, // 근무 시작 시간
endHour: 18, // 근무 종료 시간
lunchStart: 12, // 점심 시작 시간
lunchEnd: 13, // 점심 종료 시간
},
subTypes: [],
};
},
computed: {
// 세부 유형 선택박스를 보여줄지 여부
showSubTypeSelect() {
return this.dayCnt === 0.5 && this.subTypes.length > 0;
},
// cmmnCodes가 로드되었는지 확인
isCodesLoaded() {
return this.cmmnCodes && Object.keys(this.cmmnCodes).length > 0;
}
},
async created() {
this.pageId = this.$route.query.id;
this.cmmnCodes = await this.$defaultCodes();
for (const vcatnType of this.cmmnCodes.vcatnKndCodeList) {
if (parseFloat(vcatnType.codeValue) === 0.5) {
this.subTypes = await this.$findChildCodes(vcatnType.code);
break;
}
}
},
mounted() {
if (!this.$isEmpty(this.pageId)) {
this.findData();
}
},
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];
let sanctns = [];
for (let sanctn of this.editData.sanctnList) {
let data = {
confmerId: sanctn.confmerId,
clsf: sanctn.clsf,
sanctnOrdr: sanctn.sanctnOrdr,
sanctnSe: sanctn.sanctnSe,
clsfNm: sanctn.confmerInfo?.clsf,
userNm: sanctn.confmerInfo?.userNm
}
sanctns.push(data);
}
this.editData.sanctnList = sanctns;
await this.fnOnchangeVcatnKnd();
} catch (error) {
console.error('데이터 조회 실패:', error);
const message = error.response?.data?.message || "데이터를 불러오는데 실패했습니다.";
alert(message);
this.fnMoveTo('list');
}
},
// 유형 변경
async fnOnchangeVcatnKnd() {
if (!this.isCodesLoaded || !Array.isArray(this.cmmnCodes.vcatnKndCodeList)) {
console.warn('cmmnCodes가 초기화되지 않았습니다.');
return;
}
const selectedVcatn = this.cmmnCodes.vcatnKndCodeList.find(item => item.code === this.editData.vcatnKnd);
if (!selectedVcatn) {
console.warn('선택된 휴가 유형을 찾을 수 없습니다.');
return;
}
this.dayCnt = parseFloat(selectedVcatn.codeValue);
if (this.dayCnt === 0.5) {
this.changeBgnde(); // 반차일 경우 종료일을 시작일과 동일하게 설정
} else {
this.editData.vcatnSubKnd = ''; // 세부 유형 선택 초기화
// 전체 근무시간으로 설정
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();
}
},
// 세부 유형 변경
fnOnchangeVcatnSubKnd() {
if (this.dayCnt === 0.5) {
const selectedSubType = this.subTypes.find(item => item.code === this.editData.vcatnSubKnd);
if (selectedSubType) {
this.setHalfDayTime(selectedSubType);
}
}
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 = {
confmerId: user.userId,
clsf: user.clsf,
sanctnOrdr: this.editData.nmprList.length + 1,
sanctnSe: this.cmmnCodes.sanctnCodeList[0].code,
clsfNm: this.formatClsf(user.clsf),
userNm: user.userNm,
};
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;
});
},
// 직급 매칭
formatClsf(code) {
const clsfCode = this.cmmnCodes?.clsfCodeList?.find(item => item.code === code);
return clsfCode?.codeNm || code;
},
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;
}
if (this.dayCnt === 0.5) {
if (this.$isEmpty(this.editData.vcatnSubKnd)) {
alert("세부 유형을 선택해 주세요.");
return false;
}
}
return true;
},
async fnSave() {
try {
if (!this.validateForm()) {
return;
}
if (!this.$isEmpty(this.pageId)) {
this.editData.confm_at = 'W';
}
const response = this.$isEmpty(this.pageId) ? await saveVcatnProc(this.editData) : await updateVcatnProc(this.editData);
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>