
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"><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="시" v-model="editData.beginHour" readonly />
<input type="text" class="form-control" placeholder="분" 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="시" v-model="editData.endHour" readonly />
<input type="text" class="form-control" placeholder="분" 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" :sanctns="editData.sanctnList" @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="reset" 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/SanctnList.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;
}
},
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(); // calculateTotalDays 대신 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) {
console.log("sanctn: ", sanctn);
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;
// cmmnCodes가 초기화되었는지 확인 후 호출
if (this.cmmnCodes && this.cmmnCodes.vcatnKndCodeList) {
await this.fnOnchangeVcatnKnd();
} else {
console.warn('cmmnCodes가 아직 초기화되지 않았습니다.');
}
} catch (error) {
console.error('데이터 조회 실패:', error);
const message = error.response?.data?.message || "데이터를 불러오는데 실패했습니다.";
alert(message);
this.fnMoveTo('list');
}
},
// 유형 변경
async fnOnchangeVcatnKnd() {
// 안전성 검사 추가
if (!this.cmmnCodes || !this.cmmnCodes.vcatnKndCodeList || !Array.isArray(this.cmmnCodes.vcatnKndCodeList)) {
console.warn('cmmnCodes.vcatnKndCodeList가 초기화되지 않았거나 배열이 아닙니다.');
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())) {
console.warn('유효하지 않은 날짜입니다.');
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(data) {
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;
}
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>