
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 vcatnKnds" :key="idx" :value="item.code">{{ item.codeNm }}</option>
</select>
</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" :lists="editData.sanctnList" @onSelected="fnAddSanctn" @close="isOpenModal = false" />
<div class="approval-container">
<SanctnList v-model:lists="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: 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>