
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>출장구분 *</th>
<td>
<select class="form-select sm" style="width: 110px;" v-model="bsrpInfo.bsrpSe">
<option v-for="(item, idx) of bsrpCodes" :key="idx" :value="item.code"> {{ item.codeNm }} </option>
</select>
</td>
<th>일수</th>
<td>
<input type="text" class="form-control sm" v-model="totalDays" readonly />
</td>
</tr>
<tr>
<th>출장지 *</th>
<td>
<input type="text" class="form-control sm" v-model="bsrpInfo.bsrpPlace" />
</td>
<th>출장목적 *</th>
<td>
<input type="text" class="form-control sm" v-model="bsrpInfo.bsrpPurps" />
</td>
</tr>
<tr>
<th>출장기간 *</th>
<td colspan="3">
<div class="d-flex">
<div class="d-flex gap-1 mb-1">
<input type="date" class="form-control sm" v-model="bsrpInfo.bgnde" />
<input type="text" class="form-control sm" style="width: 100px;" placeholder="시" v-model="bsrpInfo.beginHour" maxlength="2" @input="validateHour('beginHour', $event)" />
<input type="text" class="form-control sm" style="width: 100px;" placeholder="분" v-model="bsrpInfo.beginMnt" maxlength="2" @input="validateMinute('beginMnt', $event)" />
</div>
<div class="d-flex gap-1">
<input type="date" class="form-control sm" v-model="bsrpInfo.endde" />
<input type="text" class="form-control sm" style="width: 100px;" placeholder="시" v-model="bsrpInfo.endHour" maxlength="2" @input="validateHour('endHour', $event)" />
<input type="text" class="form-control sm" style="width: 100px;" placeholder="분" v-model="bsrpInfo.endMnt" maxlength="2" @input="validateMinute('endMnt', $event)" />
</div>
</div>
</td>
</tr>
<tr>
<th>동행자</th>
<td>
<button type="button" title="추가" @click="isOpenNmprModal = true">
<PlusCircleFilled />
</button>
<HrPopup v-if="isOpenNmprModal" :selectedEmployees="bsrpInfo.bsrpNmprList" idField="triperId" :dateInfo="bsrpInfo" @select="handleCompanionAdd" @close="isOpenNmprModal = false" />
<div class="approval-container">
<div v-for="(item, idx) of bsrpInfo.bsrpNmprList" :key="idx" class="d-flex addapproval">
<div class="d-flex align-items-center border-x">
<p>{{ item.triperNm }} {{ item.clsfNm }}</p>
<button type="button" @click="handleCompanionRemove(idx)" @mousedown.stop>
<CloseOutlined />
</button>
</div>
</div>
</div>
</td>
<th>승인자 *</th>
<td>
<button type="button" title="추가" @click="isOpenSanctnModal = true">
<PlusCircleFilled />
</button>
<HrPopup v-if="isOpenSanctnModal" :selectedEmployees="bsrpCnsul.sanctnList" idField="confmerId" @select="handleApproverAdd" @close="isOpenSanctnModal = false" />
<div class="approval-container">
<SanctnList v-model:lists="bsrpCnsul.sanctnList" @delSanctn="handleApproverRemove" />
</div>
</td>
</tr>
<tr>
<th>품의내용 *</th>
<td colspan="3" style="height: calc(100% - 550px);">
<EditorComponent v-model:contents="bsrpCnsul.cn" />
</td>
</tr>
<tr>
<th>법인카드</th>
<td>
<button type="button" title="추가" @click="isOpenCardModal = true">
<PlusCircleFilled />
</button>
<CorpCardPopup v-if="isOpenCardModal" :bsrpInfo="bsrpInfo" :lists="cards" @close="isOpenCardModal = false" @onSelected="handleCardAdd" />
<div class="approval-container">
<div v-for="(card, idx) in cards" :key="idx" class="d-flex gap-2 addapproval mb-2">
<form class="d-flex align-items-center border-x">
<p>{{ card.cardNm }}</p>
<button type="button" @click="handleCardRemove(idx)" class="delete-button">
<CloseOutlined />
</button>
</form>
</div>
</div>
</td>
<th>법인차량</th>
<td>
<button type="button" title="추가" @click="isOpenVhcleModal = true">
<PlusCircleFilled />
</button>
<CorpCarPopup v-if="isOpenVhcleModal" :bsrpInfo="bsrpInfo" :lists="vhcles" @close="isOpenVhcleModal = false" @onSelected="handleVehicleAdd" />
<div class="approval-container">
<div v-for="(vhcle, idx) in vhcles" :key="idx" class="d-flex gap-2 addapproval mb-2">
<p>{{ vhcle.vhcleNm }}</p>
<select class="form-select" v-model="vhcle.drverId">
<option value="" disabled hidden>운전자 선택</option>
<option :value="userInfo.userId">{{ userInfo.userNm }}</option>
<option v-for="(item, idx) of bsrpInfo.bsrpNmprList" :key="idx" :value="item.userId"> {{ item.userNm }} </option>
</select>
<button type="button" @click="handleVehicleRemove(idx)" class="delete-button">
<CloseOutlined />
</button>
</div>
</div>
</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" @click="handleUpdate"> {{ submitButtonText }} </button>
<button type="button" class="btn sm secondary" @click="handleCancel">취소</button>
</template>
</div>
</div>
</div>
</template>
<script>
import { PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue';
import HrPopup from '../../../component/Popup/HrPopup.vue';
import CorpCarPopup from '../../../component/Popup/CorpCarPopup.vue';
import CorpCardPopup from '../../../component/Popup/CorpCardPopup.vue';
import SanctnList from '../../../component/Sanctn/SanctnFormList.vue';
import EditorComponent from '../../../component/editor/EditorComponent.vue';
// API
import { saveBsrpProc, findBsrpProc, updateBsrpProc, findLastBsrpCnsulProc } from '../../../../resources/api/bsrp';
export default {
name: 'BsrpInsert',
components: {
PlusCircleFilled, CloseOutlined,
HrPopup, CorpCarPopup, CorpCardPopup, SanctnList, EditorComponent,
},
data() {
return {
pageId: null,
pageMode: null,
userInfo: this.$store.state.userInfo,
isOpenNmprModal: false,
isOpenSanctnModal: false,
isOpenCardModal: false,
isOpenVhcleModal: false,
bsrpCodes: [],
sanctnCodes: [],
defaultSanctnCode: null,
bsrpInfo: {
bsrpId: null,
applcntId: null,
bsrpSe: null,
bsrpSeNm: null,
bsrpPlace: null,
bsrpPurps: null,
bgnde: null,
beginHour: null,
beginMnt: null,
endde: null,
endHour: null,
endMnt: null,
bsrpNmprList: []
},
cards: [],
vhcles: [],
bsrpCnsul: {
bsrpId: null,
cn: null,
rgsde: null,
register: null,
updde: null,
updusr: null,
sanctnList: []
},
totalDays: 0,
};
},
watch: {
// 시작일 변동 시 날짜 재계산
'bsrpInfo.bgnde'() {
this.calculateDays();
},
// 종료일 변동 시 날짜 재계산
'bsrpInfo.endde'() {
this.calculateDays();
},
},
computed: {
// 재신청 여부
isReapplyMode() {
return this.pageMode === 'reapply';
},
// 재신청 여부에 따른 등록 버튼 변경
submitButtonText() {
return this.isReapplyMode ? '재신청' : '수정';
}
},
async created() {
this.pageId = this.$route.query.id;
this.pageMode = this.$route.query.type;
this.bsrpCodes = await this.$findChildCodes('sanctn_mby_bsrp'); // 출장 구분 코드 조회
this.bsrpInfo.bsrpSe = this.bsrpCodes[0].code;
this.sanctnCodes = await this.$findChildCodes('sanctn_code'); // 결재 구분 코드 조회
this.defaultSanctnCode = this.sanctnCodes[0].code;
},
mounted() {
if (!this.$isEmpty(this.pageId)) {
this.fetchData(); // 출장 정보 조회
}
},
methods: {
// 출장 정보 조회
async fetchData() {
try {
const response = await findBsrpProc(this.pageId);
const result = response.data.data;
this.bsrpInfo = result.bsrpInfoDTO;
this.cards = result.cards;
this.vhcles = result.vhcles;
this.bsrpCnsul = result.bsrpCnsulDTO;
} catch (error) {
alert(error.response.data.message);
this.handleNavigation('list');
}
},
// 이전 승인자 조회 핸들러
async handleLoadLastApprovers() {
try {
const response = await findLastBsrpCnsulProc();
const result = response.data.data;
this.bsrpCnsul.sanctnList = result;
} catch (error) {
const message = error.response?.data?.message || "이전 승인자를 불러오는데 실패했습니다.";
alert(message);
}
},
// 저장 핸들러
async handleSave() {
try {
if (!this.validateForm()) {
return;
}
let data = this.buildSendData("insert");
const response = await saveBsrpProc(data);
alert("등록되었습니다.");
this.handleNavigation('view', response.data.data.pageId);
} catch (error) {
this.handleError(error);
}
},
// 수정 핸들러
async handleUpdate() {
try {
if (!this.validateForm()) {
return;
}
let data = this.buildSendData("update");
const response = await updateBsrpProc(this.pageId, data);
const message = this.isReapplyMode ? "재신청되었습니다." : "수정되었습니다.";
alert(message);
this.handleNavigation('view', response.data.data.pageId);
} catch (error) {
this.handleError(error);
}
},
// 동행자 추가 핸들러
handleCompanionAdd(item) {
const data = {
triperId: item.userId,
triperNm: item.userNm,
deptId: item.deptId,
deptNm: item.deptNm,
clsf: item.clsf,
clsfNm: item.clsfNm,
};
this.bsrpInfo.bsrpNmprList.push(data);
this.isOpenNmprModal = false;
},
// 동행자 삭제 핸들러
handleCompanionRemove(idx) {
this.bsrpInfo.bsrpNmprList.splice(idx, 1);
},
// 승인자 추가 핸들러
handleApproverAdd(user) {
const data = {
confmerId: user.userId,
confmerNm: user.userNm,
clsf: user.clsf,
clsfNm: user.clsfNm,
sanctnOrdr: this.bsrpCnsul.sanctnList.length + 1,
sanctnIem: 'bsrp_cnsul',
sanctnSe: this.defaultSanctnCode,
};
this.bsrpCnsul.sanctnList.push(data);
this.isOpenSanctnModal = false;
},
// 승인자 삭제 핸들러
handleApproverRemove(idx) {
this.bsrpCnsul.sanctnList.splice(idx, 1);
this.bsrpCnsul.sanctnList.forEach((item, index) => {
item.sanctnOrdr = index + 1;
});
},
// 법인 카드 추가 핸들러
handleCardAdd(item) {
this.cards.push(item);
this.isOpenCardModal = false;
},
// 법인 카드 삭제 핸들러
handleCardRemove(idx) {
this.cards.splice(idx, 1);
},
// 법인 차량 추가 핸들러
handleVehicleAdd(item) {
item.drverId = this.userInfo.userId;
this.vhcles.push(item);
this.isOpenVhcleModal = false;
},
// 법인 차량 삭제 핸들러
handleVehicleRemove(idx) {
this.vhcles.splice(idx, 1);
},
// 취소 핸들러
handleCancel() {
if (confirm('작성 중인 내용이 삭제됩니다. 계속하시겠습니까?')) {
this.handleNavigation('view', this.pageId);
}
},
// 페이지 이동 핸들러
handleNavigation(type, id) {
const routeMap = {
'list': { name: 'BsrpListPage' },
'view': { name: 'BsrpViewPage', query: { id } },
'bsrpInsert': { name: 'BsrpInsertPage', query: this.$isEmpty(id) ? {} : { id } },
'bsrpReapply': { name: 'BsrpInsertPage', query: { id, type: 'reapply' } },
'bsrpRportInsert': { name: 'BsrpRportInsertPage', query: this.$isEmpty(id) ? {} : { id } },
'bsrpRportReapply': { name: 'BsrpRportInsertPage', query: { id, type: 'reapply' } },
};
const route = routeMap[type];
if (route) {
this.$router.push(route);
} else {
alert("올바르지 않은 경로를 요청하여 목록으로 이동합니다.");
this.$router.push({ name: 'BsrpListPage' });
}
},
// 에러 핸들러
handleError(error) {
const message = error.response?.data?.message || "에러가 발생했습니다.";
alert(message);
},
// 일수 계산 유틸리티
calculateDays() {
if (!this.bsrpInfo.bgnde || !this.bsrpInfo.endde) {
this.totalDays = 0;
return;
}
const startDate = new Date(this.bsrpInfo.bgnde);
const endDate = new Date(this.bsrpInfo.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;
this.totalDays = Math.max(0, dayDiff);
},
// 출장 시간 유효성 검사 유틸리티
validateHour(field, event) {
let value = event.target.value.replace(/[^0-9]/g, '');
if (value.length > 0) {
const hour = parseInt(value);
if (hour > 23) {
value = '23';
}
}
this.bsrpInfo[field] = value;
},
// 출장 분 유효성 검사 유틸리티
validateMinute(field, event) {
let value = event.target.value.replace(/[^0-9]/g, '');
if (value.length > 0) {
const minute = parseInt(value);
if (minute > 59) {
value = '59';
}
}
this.bsrpInfo[field] = value;
},
// 입력값 전체 유효성 검사 유틸리티
validateForm() {
if (this.$isEmpty(this.bsrpInfo.bsrpSe)) {
alert('출장구분을 선택해주세요.');
return false;
}
if (this.$isEmpty(this.bsrpInfo.bsrpPlace)) {
alert('출장지를 입력해주세요.');
return false;
}
if (this.$isEmpty(this.bsrpInfo.bsrpPurps)) {
alert('출장목적을 입력해주세요.');
return false;
}
if (this.$isEmpty(this.bsrpInfo.bgnde) || this.$isEmpty(this.bsrpInfo.beginHour) || this.$isEmpty(this.bsrpInfo.beginMnt)) {
alert('출장 시작일시를 모두 입력해주세요.');
return false;
}
if (this.$isEmpty(this.bsrpInfo.endde) || this.$isEmpty(this.bsrpInfo.endHour) || this.$isEmpty(this.bsrpInfo.endMnt)) {
alert('출장 종료일시를 모두 입력해주세요.');
return false;
}
if (!this.validateTimeRange(this.bsrpInfo.beginHour, this.bsrpInfo.beginMnt, '시작')) {
return false;
}
if (!this.validateTimeRange(this.bsrpInfo.endHour, this.bsrpInfo.endMnt, '종료')) {
return false;
}
if (this.$isEmpty(this.bsrpCnsul.sanctnList) || this.bsrpCnsul.sanctnList.length === 0) {
alert('승인자를 선택해주세요.');
return false;
}
if (this.$isEmpty(this.bsrpCnsul.cn)) {
alert('품의내용을 입력해주세요.');
return false;
}
return true;
},
// 출장 기간 유효성 검사 유틸리티
validateTimeRange(hour, minute, timeType) {
const hourNum = parseInt(hour);
const minuteNum = parseInt(minute);
if (isNaN(hourNum) || hourNum < 0 || hourNum > 23) {
alert(`${timeType} 시간은 0-23 사이의 값을 입력해주세요.`);
return false;
}
if (isNaN(minuteNum) || minuteNum < 0 || minuteNum > 59) {
alert(`${timeType} 분은 0-59 사이의 값을 입력해주세요.`);
return false;
}
return true;
},
// 데이터 가공 유틸리티
buildSendData(type) {
let bsrpInfo = {
applcntId: this.bsrpInfo.applcntId,
bsrpSe: this.bsrpInfo.bsrpSe,
bsrpPlace: this.bsrpInfo.bsrpPlace,
bsrpPurps: this.bsrpInfo.bsrpPurps,
bgnde: this.bsrpInfo.bgnde,
beginHour: this.bsrpInfo.beginHour,
beginMnt: this.bsrpInfo.beginMnt,
endde: this.bsrpInfo.endde,
endHour: this.bsrpInfo.endHour,
endMnt: this.bsrpInfo.endMnt,
bsrpNmprList: this.bsrpInfo.bsrpNmprList,
}
let cards = [];
for (let card of this.cards) {
cards.push({
cardId: card.cardId,
})
}
let vhcles = [];
for (let vhcle of this.vhcles) {
vhcles.push({
vhcleId: vhcle.vhcleId,
drverId: vhcle.drverId,
})
}
let sanctnList = [];
for (let sanctn of this.bsrpCnsul.sanctnList) {
sanctnList.push({
confmerId: sanctn.confmerId,
clsf: sanctn.clsf,
sanctnOrdr: sanctn.sanctnOrdr,
sanctnIem: sanctn.sanctnIem,
sanctnSe: sanctn.sanctnSe,
})
}
let bsrpCnsul = {
cn: this.bsrpCnsul.cn,
sanctnList: sanctnList,
}
if (type === "insert") {
return {
insertBsrpInfoDTO: bsrpInfo,
cardDtlsList: cards,
vhcleDtlsList: vhcles,
insertBsrpCnsulDTO: bsrpCnsul,
};
} else if (type === "update") {
return {
updateBsrpInfoDTO: bsrpInfo,
cardDtlsList: cards,
vhcleDtlsList: vhcles,
updateBsrpCnsulDTO: bsrpCnsul,
};
}
},
},
};
</script>