
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>{{ bsrpInfo.bsrpSeNm }}</td>
</tr>
<tr>
<th>출장지</th>
<td>{{ bsrpInfo.bsrpPlace }}</td>
</tr>
<tr>
<th>출장목적</th>
<td>{{ bsrpInfo.bsrpPurps }}</td>
</tr>
<tr>
<th>출장기간</th>
<td>{{ $formattedDates(bsrpInfo) }}</td>
</tr>
<tr>
<th>동행자</th>
<td>
<span v-for="(item, idx) of bsrpInfo.bsrpNmprList" :key="idx"> {{ item.triperNm }}{{ idx !== bsrpInfo.bsrpNmprList.length - 1 ? ', ' : '' }} </span>
</td>
</tr>
<tr>
<th>법인카드</th>
<td>
<span v-for="(item, idx) of cards" :key="idx">{{ item.cardNm }}</span>
</td>
</tr>
<tr>
<th>법인차량</th>
<td>
<span v-for="(item, idx) of vhcles" :key="idx">{{ item.vhcleNm }}</span>
</td>
</tr>
<tr>
<th>승인자 *</th>
<td>
<button type="button" title="추가" @click="isOpenSanctnModal = true">
<PlusCircleFilled />
</button>
<HrPopup v-if="isOpenSanctnModal" :selectedEmployees="bsrpRport.sanctnList" idField="confmerId" @select="handleApproverAdd" @close="isOpenSanctnModal = false" />
<div class="approval-container">
<SanctnList v-model:lists="bsrpRport.sanctnList" @delSanctn="handleApproverRemove" />
</div>
</td>
</tr>
<tr>
<th>복명내용 *</th>
<td style="height: calc(100% - 550px);">
<EditorComponent v-model:contents="bsrpRport.cn" />
</td>
</tr>
<tr>
<th>여비계산</th>
<td>
<button type="button" title="추가" @click="handleExpenseAdd">
<PlusCircleFilled />
</button>
<div v-for="(item, idx) of bsrpRport.bsrpTrvctList" :key="idx" class="d-flex gap-2 addapproval mb-2">
<select class="form-select" v-model="item.se" style="width: 140px;">
<option value="" disabled hidden>결제 방식</option>
<option v-for="code of trvctTypeCodes" :key="code.code" :value="code.code"> {{ code.codeNm }} </option>
</select>
<select class="form-select" v-model="item.setleMbyId" style="width: 140px;">
<option value="" disabled hidden>결제 주체</option>
<template v-if="item.se === 'PERSONAL'">
<option :value="userInfo.userId">{{ userInfo.userNm }}</option>
<option v-for="nmpr of bsrpInfo.bsrpNmprList" :key="nmpr.userId" :value="nmpr.userId"> {{ nmpr.triperNm }} </option>
</template>
<template v-else-if="item.se === 'CORPORATE'">
<option v-for="card of cards" :key="card.cardId" :value="card.cardId"> {{ card.cardNm }} </option>
</template>
</select>
<select class="form-select" v-model="item.ty" style="width: 140px;">
<option value="" disabled hidden>출장비 구분</option>
<option v-for="expense of travelExpenseCodes" :key="expense.code" :value="expense.code"> {{ expense.codeNm }} </option>
</select>
<input type="number" class="form-control" placeholder="금액입력" v-model="item.amount" style="max-width: 150px;" />
<div>
<label :for="'fileUpload-' + idx" class="btn sm primary">영수증 첨부</label>
<input :id="'fileUpload-' + idx" type="file" @change="handleFileUpload(idx, $event)" class="hidden-file-input" accept="image/*,.pdf" />
<span v-if="item.fileName" class="file-name">{{ item.fileName }}</span>
</div>
<button type="button" @click="handleExpenseRemove(idx)" class="delete-button">
<CloseOutlined />
</button>
</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="!this.hasBsrpRport" @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 SanctnList from '../../../component/Sanctn/SanctnFormList.vue';
import EditorComponent from '../../../component/editor/EditorComponent.vue';
// API
import { findBsrpProc } from '../../../../resources/api/bsrp';
import { findBsrpRportProc, saveBsrpRportProc, updateBsrpRport, findLastBsrpRportProc } from '../../../../resources/api/bsrpRport';
export default {
name: 'BsrpRportInsert',
components: {
PlusCircleFilled,
CloseOutlined,
HrPopup,
SanctnList,
EditorComponent
},
data() {
return {
pageId: null,
pageMode: null,
userInfo: this.$store.state.userInfo,
isOpenSanctnModal: false,
sanctnCodes: [],
defaultSanctnCode: null,
trvctTypeCodes: [],
travelExpenseCodes: [],
bsrpInfo: {
bsrpNmprList: []
},
cards: [],
vhcles: [],
hasBsrpRport: false,
bsrpRport: {
cn: null,
fileId: null,
bsrpTrvctList: [],
sanctnList: [],
},
};
},
computed: {
// 재신청 여부
isReapplyMode() {
return this.pageMode === 'reapply';
},
// 재신청 여부에 따른 등록 버튼 변경
submitButtonText() {
return this.isReapplyMode ? '재신청' : '수정';
}
},
async created() {
this.pageId = this.$route.query.id;
this.pageMode = this.$route.query.type;
if (this.$isEmpty(this.pageId)) {
alert("게시물이 존재하지 않습니다.");
this.handleNavigation('list');
return;
}
this.sanctnCodes = await this.$findChildCodes('sanctn_code');
this.defaultSanctnCode = this.sanctnCodes[0]?.code;
this.trvctTypeCodes = await this.$findChildCodes('BSRP_TRVCT');
this.travelExpenseCodes = await this.$findChildCodes('TRAVEL_EXPENSE');
},
async mounted() {
await 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.hasBsrpRport = result.hasBsrpRport;
if (this.hasBsrpRport) {
this.fetchReportData(); // 출장 복명 정보 조회
}
} catch (error) {
alert('데이터 조회에 실패했습니다.');
this.handleNavigation('list');
}
},
// 출장 복명 정보 조회
async fetchReportData() {
try {
const response = await findBsrpRportProc(this.pageId);
const result = response.data.data;
this.bsrpRport = result;
if (this.bsrpRport.bsrpTrvctList && this.bsrpRport.fileList) {
this.bsrpRport.bsrpTrvctList.forEach(trvct => {
const file = this.bsrpRport.fileList.find(f => f.ordr === trvct.ordr);
if (file) {
trvct.fileName = file.fileNm;
trvct.isExistingFile = true;
} else {
this.initializeExpenseFile(trvct);
}
});
}
} catch (error) {
alert('데이터 조회에 실패했습니다.');
this.handleNavigation('list');
}
},
// 이전 승인자 조회 핸들러
async handleLoadLastApprovers() {
try {
const response = await findLastBsrpRportProc();
const result = response.data.data;
if (result == null || result.length == 0) {
alert("출장 복명 기록이 존재하지 않아, 이전 승인자를 불러올 수 없습니다.");
return;
}
this.bsrpRport.sanctnList = result;
} catch (error) {
const message = error.response?.data?.message || "이전 승인자를 불러오는데 실패했습니다.";
alert(message);
}
},
// 저장 핸들러
async handleSave() {
if (!this.validateForm()) return;
try {
const formData = this.buildFormData();
this.bsrpRport.bsrpTrvctList.forEach(item => {
if (item.file) {
formData.append('files', item.file);
}
});
await saveBsrpRportProc(formData);
alert('출장 복명서가 등록되었습니다.');
this.handleNavigation('view', this.pageId);
} catch (error) {
alert(error.response?.data?.message || '저장에 실패했습니다.');
}
},
// 수정 핸들러
async handleUpdate() {
if (!this.validateForm()) return;
try {
const formData = this.buildFormData();
const oldFiles = [];
const newFileItems = [];
this.bsrpRport.bsrpTrvctList.forEach(item => {
if (item.isExistingFile && item.ordr > 0) {
oldFiles.push(String(item.ordr));
} else if (item.file) {
newFileItems.push(item);
}
});
formData.append('oldFiles', new Blob([JSON.stringify(oldFiles)], { type: 'application/json' }));
newFileItems.forEach(item => {
formData.append('files', item.file);
});
await updateBsrpRport(formData);
const message = this.isReapplyMode ? "재신청되었습니다." : "수정되었습니다.";
alert(message);
this.handleNavigation('view', this.pageId);
} catch (error) {
alert(error.response?.data?.message || '수정에 실패했습니다.');
}
},
// 승인자 추가 핸들러
handleApproverAdd(user) {
this.bsrpRport.sanctnList.push({
confmerId: user.userId,
confmerNm: user.userNm,
clsf: user.clsf,
clsfNm: user.clsfNm,
sanctnOrdr: this.bsrpRport.sanctnList.length + 1,
sanctnIem: 'bsrp_rport',
sanctnSe: this.defaultSanctnCode,
});
this.isOpenSanctnModal = false;
},
// 승인자 삭제 핸들러
handleApproverRemove(idx) {
this.bsrpRport.sanctnList.splice(idx, 1);
this.bsrpRport.sanctnList.forEach((item, index) => {
item.sanctnOrdr = index + 1;
});
},
// 여비 계산 추가 핸들러
handleExpenseAdd() {
this.bsrpRport.bsrpTrvctList.push({
se: this.cards.length > 0 ? 'CORPORATE' : 'PERSONAL',
setleMbyId: this.cards.length > 0 ? this.cards[0].cardId : this.userInfo.userId,
ty: '',
amount: null,
ordr: 0,
file: null,
fileName: '',
isExistingFile: false
});
},
// 여비 계산 삭제 핸들러
handleExpenseRemove(idx) {
this.bsrpRport.bsrpTrvctList.splice(idx, 1);
},
// 파일 업로드 핸들러
handleFileUpload(idx, event) {
const file = event.target.files[0];
if (!file) return;
const trvct = this.bsrpRport.bsrpTrvctList[idx];
trvct.file = file;
trvct.fileName = file.name;
trvct.isExistingFile = false;
},
// 취소 핸들러
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(routeMap['list']);
}
},
// 파일 정보 초기화 유틸리티
initializeExpenseFile(trvct) {
trvct.ordr = 0;
trvct.isExistingFile = false;
trvct.file = null;
trvct.fileName = '';
},
// 입력값 전체 유효성 검사 유틸리티
validateForm() {
if (!this.bsrpRport.cn?.trim()) {
alert('복명내용을 입력해주세요.');
return false;
}
if (this.bsrpRport.sanctnList.length === 0) {
alert('승인자를 선택해주세요.');
return false;
}
for (let [idx, item] of this.bsrpRport.bsrpTrvctList.entries()) {
const num = idx + 1;
if (!item.se) {
alert(`${num}번째 여비계산의 결제 방식을 선택해주세요.`);
return false;
}
if (!item.setleMbyId) {
alert(`${num}번째 여비계산의 결제 주체를 선택해주세요.`);
return false;
}
if (!item.ty) {
alert(`${num}번째 여비계산의 출장비 구분을 선택해주세요.`);
return false;
}
if (!item.amount || item.amount <= 0) {
alert(`${num}번째 여비계산의 금액을 입력해주세요.`);
return false;
}
if (!item.fileName) {
alert(`${num}번째 여비계산의 영수증을 첨부해주세요.`);
return false;
}
}
return true;
},
// 데이터 가공 유틸리티
buildFormData() {
const formData = new FormData();
formData.append('bsrpId', this.pageId);
const dto = {
cn: this.bsrpRport.cn,
sanctnList: this.bsrpRport.sanctnList.map(s => ({
confmerId: s.confmerId,
clsf: s.clsf,
sanctnOrdr: s.sanctnOrdr,
sanctnIem: s.sanctnIem,
sanctnSe: s.sanctnSe,
})),
bsrpTrvctList: this.bsrpRport.bsrpTrvctList.map(t => ({
se: t.se,
setleMbyId: t.setleMbyId,
ty: t.ty,
amount: t.amount,
ordr: t.ordr || 0,
}))
};
if (this.bsrpRport.fileId) {
dto.fileId = this.bsrpRport.fileId;
}
formData.append('bsrpRportDTO', new Blob([JSON.stringify(dto)], { type: 'application/json' }));
return formData;
},
},
};
</script>