
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>
<div class="name-box flex simple">
<div class="img-area column">
<div class="img">
<img :src="croppedImg || placeholder" alt="미리보기 이미지" />
<button class="close-btn" @click="removeImage">×</button>
</div>
<div class="info">
<div class="file">
<label for="fileUpload" class="file-label">
<img :src="file" alt="">
<button @click="showCropModal = true">
<p>업로드</p>
</button>
</label>
</div>
</div>
</div>
<ImageCropper :key="cropperKey" field="avatar" v-model="showCropModal" :width="200" :height="200"
img-format="png" lang-type="ko" @crop-success="cropSuccess" :no-square="true" :round-cropping="true"
:max-size="1024" />
<div class="tbl-wrap row g-3 needs-validation " :class="{ 'was-validated': formSubmitted }">
<div class="col-12 ">
<div class="col-12 border-x">
<label for="youremail" class="form-label ">
<p>아이디
<p class="require"><img :src="require" alt=""></p>
</p>
</label>
<input v-if="isUserIdCheck" v-model="selectedUser.loginId" type="text" name="loginId" class="form-control"
required readonly>
<input v-else v-model="requestDTO.loginId" type="text" name="loginId" class="form-control" required>
</div>
<div class="col-12 border-x">
<label for="yourPassword" class="form-label">
<p>권한
<p class="require"><img :src="require" alt=""></p>
</p>
</label>
<select class="form-select" v-model="selectedAuthor">
<option value="">선택</option>
<option v-for="author in authorCode" :key="author.authorNm" :value="author.authorCode">
{{ author.authorNm }}
</option>
</select>
</div>
</div>
<div class="col-12 ">
<div class="col-12 border-x">
<label for="youremail" class="form-label ">
<p>이름
<p class="require"><img :src="require" alt=""></p>
</p>
</label>
<input v-model="requestDTO.userNm" type="text" name="userNm" class="form-control" required>
</div>
<div class="col-12 border-x">
<label for="yourPassword" class="form-label">
<p>부서
<p class="require"><img :src="require" alt=""></p>
</p>
</label>
<input v-model="selectedname" type="text" name="text" class="form-control" required>
<input type="button" class="form-control " value="검색" @click="showPopup = true" />
<BuseoPopup v-if="showPopup" @close="showPopup = false" @select="addApproval" />
</div>
</div>
<div class="col-12 border-x">
<div class="col-12 border-x">
<label for="youremail" class="form-label">
<p>직급
<p class="require"><img :src="require" alt=""></p>
</p>
</label>
<select class="form-select" v-model="selectedClsf">
<option value="">선택</option>
<option v-for="jobRank in clsfCode" :key="jobRank.codeNm" :value="jobRank.code">
{{ jobRank.codeNm }}
</option>
</select>
</div>
<div class="col-12 border-x">
<label for="yourPassword" class="form-label">직책</label>
<select class="form-select" v-model="selectedRepofc">
<option value="">선택</option>
<option v-for="position in rspofcCode" :key="position.codeNm" :value="position.code">
{{ position.codeNm }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="tbl-wrap row g-3 needs-validation detail" :class="{ 'was-validated': formSubmitted }"
style="margin-bottom: 20px;">
<div class="col-12">
<label for="phone" class="form-label">연락처</label>
<input v-model="requestDTO.mbtlnum" type="text" name="mbtlnum" class="form-control" id="phone"
placeholder="010-1234-5678" @input="filterPhoneNumber">
</div>
<div class="col-12">
<label for="birth" class="form-label">생년월일</label>
<input v-model="requestDTO.brthdy" type="date" name="brthdy" class="form-control" id="yourName">
</div>
<div class="col-12 border-x">
<label for="yourName" class="form-label">
<p>입사일
<p class="require"><img :src="require" alt=""></p>
</p>
</label>
<input v-model="requestDTO.encpn" type="date" name="encpn" class="form-control" id="yourName" required>
</div>
</div>
<div class="tbl-wrap row g-3 needs-validation detail salary" :class="{ 'was-validated': formSubmitted }">
<div class=" col-12 border-x"><label>연봉<button type="button" title="추가" @click="addSalary">
<PlusCircleFilled />
</button></label>
<div class="yearsalary approval-container">
<div class="col-12 border-x addapproval" v-for="(salary, index) in salarys" :key="index">
<input type="text" name="name" class="form-control" v-model="salary.year" style="width: 200px;"
placeholder="년도" maxlength="4" @input="validateYearInput(salary)">
<div>
<input type="text" name="name" class="form-control" v-model="salary.amount" placeholder="금액">
</div>
<button type="button" @click="removeSalary(index)" class="delete-button">
<CloseOutlined />
</button>
</div>
</div>
</div>
</div>
<div class="buttons">
<button type="button" class="btn sm primary" @click="submitUpdateForm" v-if="isUserIdCheck">수정</button>
<button type="button" class="btn sm primary" @click="submitForm" v-else>등록</button>
<button type="button" class="btn sm tertiary" @click="goToManagementList">취소</button>
</div>
</div>
</div>
</template>
<script>
import GoogleCalendar from "../../../component/GoogleCalendar.vue"
import { SearchOutlined, PlusCircleFilled, CloseOutlined } from '@ant-design/icons-vue';
import BuseoPopup from "../../../component/Popup/BuseoPopup.vue";
import { findAuthorsProc } from "../../../../resources/api/author";
import ImageCropper from 'vue-image-crop-upload'; //이미지 자르기기
import FileUploadProgress from '@/views/component/FileUploadProgress.vue';
import { joinProc, findUsersDetProc, updateUsersProc } from "../../../../resources/api/user";
import { findCodesProc } from "../../../../resources/api/code";
export default {
data() {
return {
showPopup: false,
selectedname: '',
approvals: [],
salarys: [{
year: '',
amount: '',
},],
previewImg: null,
placeholder: "/client/resources/img/img1.png",
require: "/client/resources/img/require.png",
file: "/client/resources/img/file.png",
photoicon: "/client/resources/img/photo_icon.png",
img1: "/client/resources/img/img.png",
icon1: "/client/resources/img/icon.png",
dateicon: "/client/resources/img/date.png",
startbtn: "/client/resources/img/start.png",
stopbtn: "/client/resources/img/stop.png",
moreicon: "/client/resources/img/more.png",
listData: Array.from({ length: 20 }, (_, i) => ({
department: `부서 ${i + 1}`,
name: `이름 ${i + 1}`,
position: `직급 ${i + 1}`
})),
// 코드 조회
serachRequest: {
searchType: "upperCd",
searchText: null,
},
// 직급코드
clsfCode: [],
// 직책코드
rspofcCode: [],
// 권한 코드
authorCode: [],
showCropModal: false, // ImageCropper 모달을 보여줄지 말지 결정하는 변수
croppedImg: null, // 최종 크롭되어 표시될 이미지의 Data URL 또는 서버 이미지 URL
cropperKey: 0,
requestDTO: {
loginId: null, // @NotBlank, @Size, @Pattern
clsf: null, // 선택된 직급의 코드 (예: "RESP_JRM")
rspofc: null, // 선택된 직책의 코드 (예: "RSP_DR")
userNm: null, // @NotBlank, @Size
password: null, // @NotBlank, @Size, @Pattern (이 화면에서 password 받는다면)
mbtlnum: null, // 휴대폰
telno: null, // 전화번호 (현재 화면에 없으면 null 유지)
email: null, // 이메일 (현재 화면에 없으면 null 유지)
zip: null, // 우편번호 (현재 화면에 없으면 null 유지)
adres: null, // 주소 (현재 화면에 없으면 null 유지)
detailAdres: null, // 상세주소 (현재 화면에 없으면 null 유지)
fileId: null, // 파일 아이디 (파일 저장 후 백엔드에서 반환될 값, 직접 입력X)
brthdy: null, // 생년월일 (YYYY-MM-DD 문자열)
encpn: null, // 입사일 (YYYY-MM-DD 문자열)
authorList: [], // 선택된 권한의 ID들을 담을 배열 (UserAuthorVO 리스트 형태로 변환하여 전송)
},
selectedAuthor: '',
selectedClsf: '',
selectedRepofc: '',
copyAuthor: null,
selectedUser: {},
isUserIdCheck: false,
}
},
components: {
SearchOutlined, PlusCircleFilled, BuseoPopup, CloseOutlined, findAuthorsProc, ImageCropper, FileUploadProgress, joinProc, findCodesProc
},
created() {
if (this.$route.query.id != null) {
this.searchUser();
};
this.clsfTypeCodes(); // 직급 종류 조회
this.rspofcTypeCodes(); // 직책 종류 조회
this.authorTypeCodes(); // 권한 종류 조회
},
methods: {
//연도 4자리 숫자만 기록록
validateYearInput(salaryItem) {
// 1. 숫자 이외의 문자 제거 (정규식 사용)
salaryItem.year = salaryItem.year.replace(/[^0-9]/g, '');
// 2. 길이가 4자를 초과하면 잘라내기
if (salaryItem.year.length > 4) {
salaryItem.year = salaryItem.year.slice(0, 4);
}
// console.log("Current year value:", salaryItem.year); // 디버깅용
},
dataURLtoFile(dataurl, filename) {
var arr = dataurl.split(','),
mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]),
n = bstr.length,
u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
},
// 연락처 숫자만 기록
filterPhoneNumber() {
if (this.requestDTO.mbtlnum) {
this.requestDTO.mbtlnum = this.requestDTO.mbtlnum.replace(/\D/g, '');
}
},
// 취소 누를 시 직원관리 페이지로 이동
goToManagementList() {
this.$router.push('/hr-management/hrManagement.page');
},
// 공통코드 직급
async clsfTypeCodes() {
this.clsfCode = []; // 초기화
this.serachRequest.searchType = "UPPER_CODE"
this.serachRequest.searchText = "clsf_code"; // 공통코드 분류코드 (직급)
try {
const response = await findCodesProc(this.serachRequest);
if (response.status === 200) {
this.clsfCode = response.data.data.codes; // API 응답에서 카테고리 목록을 가져옴
}
} catch (error) {
console.error("검색 중 오류 발생:", error);
}
},
// 공통코드 직책
async rspofcTypeCodes() {
this.clsfCode = []; // 초기화
this.serachRequest.searchType = "UPPER_CODE"
this.serachRequest.searchText = "rspofc_code"; // 공통코드 분류코드 (직급)
try {
const response = await findCodesProc(this.serachRequest);
if (response.status === 200) {
this.rspofcCode = response.data.data.codes; // API 응답에서 카테고리 목록을 가져옴
}
} catch (error) {
console.error("검색 중 오류 발생:", error);
}
},
// 공통코드 권한
async authorTypeCodes() {
this.authorCode = []; // 초기화
try {
const response = await findAuthorsProc();
if (response.status === 200) {
this.authorCode = response.data.data.authors; // API 응답에서 카테고리 목록을 가져옴
}
} catch (error) {
console.error("검색 중 오류 발생:", error);
}
},
addApproval(user) {
this.approvals.push({
name: user.name
});
this.selectedname = user.department; // 입력창에 표시
this.showPopup = false;
},
addSalary() {
this.salarys.push({
year: '',
amount: '',
});
},
removeSalary(index) {
this.salarys.splice(index, 1);
},
removeImage() {
this.croppedImg = null;
this.cropperKey++;
},
cropSuccess(imgDataUrl) {
this.croppedImg = imgDataUrl;
this.showCropModal = false;
},
//등록용
async submitForm() {
// 1. 프론트엔드 유효성 검사 (네 style인 alert + return 방식)
if (this.$isEmpty(this.requestDTO.loginId)) { alert("아이디를 입력해 주세요."); return; }
if (this.$isEmpty(this.requestDTO.userNm)) { alert("이름을 입력해 주세요."); return; }
if (this.$isEmpty(this.requestDTO.clsf)) { alert("직급을 선택해 주세요."); return; }
if (this.$isEmpty(this.requestDTO.encpn)) { alert("입사일을 선택해 주세요."); return; }
if (this.$isEmpty(this.requestDTO.authorList) || this.requestDTO.authorList.length === 0) {
alert("권한을 선택해 주세요."); return;
}
// 2. FormData 객체 생성 및 데이터 추가
const formData = new FormData();
formData.append('joinDTO', new Blob([JSON.stringify(this.requestDTO)], {
type: 'application/json'
}));
formData.append('salarys', new Blob([JSON.stringify(this.salarys)], {
type: 'application/json'
}));
// 파일이 있다면 File 객체로 변환하여 'multipartFiles' 필드명으로 추가
if (this.croppedImg) {
const profileImageFile = this.dataURLtoFile(this.croppedImg, 'profile_image.png');
formData.append("multipartFiles", profileImageFile);
}
// 3. API 통신 (fileClient 사용)
try {
const response = await joinProc(formData);
if (response.status === 200) {
alert('사용자 정보가 성공적으로 저장되었습니다!');
// 성공 시 페이지 이동 (예: this.$router.push('/hr-management/hrManagement.page'))
}
} catch (error) {
console.error("사용자 정보 저장 중 오류 발생:", error);
alert("사용자 정보 저장 중 오류가 발생했습니다.");
if (error.response && error.response.data && error.response.data.message) {
alert(`오류: ${error.response.data.message}`);
}
}
},
//수정용
async submitUpdateForm() {
// 1. 프론트엔드 유효성 검사
if (this.$isEmpty(this.requestDTO.userNm)) { alert("이름을 입력해 주세요."); return; }
if (this.$isEmpty(this.requestDTO.clsf)) { alert("직급을 선택해 주세요."); return; }
if (this.$isEmpty(this.requestDTO.encpn)) { alert("입사일을 선택해 주세요."); return; }
if (this.$isEmpty(this.requestDTO.authorList) || this.requestDTO.authorList.length === 0) {
alert("권한을 선택해 주세요."); return;
}
// 2. FormData 객체 생성
const formData = new FormData();
let updateDTO = {};
if (this.selectedAuthor == this.copyAuthor) {
// 수정용 DTO 생성 (기존 데이터 + 수정 데이터)
updateDTO = {
...this.requestDTO,
salaryList: this.salarys,
authorUpdateCheck: false,
};
} else {
updateDTO = {
...this.requestDTO,
salaryList: this.salarys,
authorUpdateCheck: true,
};
}
formData.append('updateUserDTO', new Blob([JSON.stringify(updateDTO)], {
type: 'application/json'
}));
// 3. 파일 처리
// 기존 파일이 그대로 유지되는 경우 (fileId만 전송)
if (this.selectedUser.fileId && !this.croppedImg.startsWith('data:')) {
// 기존 파일 ID 유지
updateDTO.fileId = this.selectedUser.fileId;
}
// 새로운 파일이 업로드된 경우
else if (this.croppedImg && this.croppedImg.startsWith('data:')) {
const profileImageFile = this.dataURLtoFile(this.croppedImg, 'profile_image.png');
formData.append("multipartFiles", profileImageFile);
// 기존 파일 ID 제거 (새 파일로 교체)
updateDTO.fileId = null;
}
// 4. API 통신
try {
const response = await updateUsersProc(this.$route.query.id, formData);
if (response.status === 200) {
alert('사용자 정보가 성공적으로 저장되었습니다!');
// 성공 시 페이지 이동 (예: this.$router.push('/hr-management/hrManagement.page'))
}
} catch (error) {
console.error("사용자 정보 저장 중 오류 발생:", error);
alert("사용자 정보 저장 중 오류가 발생했습니다.");
if (error.response && error.response.data && error.response.data.message) {
alert(`오류: ${error.response.data.message}`);
}
}
},
//사용자 정보 상세 조회
async searchUser() {
try {
const response = await findUsersDetProc(this.$route.query.id);
if (response.status === 200) {
this.selectedUser = response.data.data.user; // API 응답에서 카테고리 목록을 가져옴
this.requestDTO = {
userNm: this.selectedUser.userNm,
clsf: this.selectedUser.clsf,
rspofc: this.selectedUser.rspofc || '',
mbtlnum: this.selectedUser.mbtlnum,
telno: this.selectedUser.telno,
email: this.selectedUser.email,
zip: this.selectedUser.zip,
adres: this.selectedUser.adres,
detailAdres: this.selectedUser.detailAdres,
brthdy: this.selectedUser.brthdy,
encpn: this.selectedUser.encpn,
userSttus: this.selectedUser.userSttus,
useAt: this.selectedUser.useAt,
fileId: this.selectedUser.fileId, // 기존 파일 ID 보존
authorList: this.selectedUser.authorList
}
this.selectedAuthor = this.selectedUser.authorList[0]?.authorCode;
this.selectedClsf = this.selectedUser.clsf;
this.selectedRepofc = this.selectedUser.rspofc || '';
this.croppedImg = this.selectedUser.thumbnail?.filePath || null;
// 연봉 정보 설정
this.salarys = this.selectedUser.salaryList || [];
this.copyAuthor = this.selectedUser.authorList[0]?.authorCode;
this.isUserIdCheck = true;
console.log("유저 정보 로드 완료", this.selectedUser);
}
} catch (error) {
console.error("검색 중 오류 발생:", error);
}
},
},
watch: {
// 직급 선택 감지 -> requestDTO.clsf 업데이트
selectedClsf(newVal) {
this.requestDTO.clsf = newVal ? newVal : null; // 선택된 직급 객체의 'code'를 requestDTO.clsf에 할당
},
// 직책 선택 감지 -> requestDTO.rspofc 업데이트
selectedRepofc(newVal) {
this.requestDTO.rspofc = newVal ? newVal : null; // 선택된 직책 객체의 'code'를 requestDTO.rspofc에 할당
},
// 권한 선택 감지 -> requestDTO.authorList 업데이트
selectedAuthor(newVal) {
if (newVal) {
// UserAuthorVO 형태에 맞춰 authorId와 authorNm을 포함하는 객체를 생성하여 배열에 담음
// 백엔드 UserAuthorVO에 정확히 authorId, authorNm 필드가 있는지 확인 필요
this.requestDTO.authorList = [{
authorCode: newVal,
}];
} else {
this.requestDTO.authorList = [];
}
},
},
computed: {
},
mounted() {
},
}
</script>
<style scoped>
tr {
cursor: pointer;
}
.profile-container {
padding: 20px;
text-align: center;
}
.info {
margin-bottom: 20px;
}
.profile-image-area {
width: 150px;
height: 150px;
border-radius: 50%;
/* 원형으로 만들게 */
overflow: hidden;
/* 영역 밖으로 넘치는 이미지 자르기 */
margin: 0 auto 20px;
/* 가운데 정렬 및 아래 여백 */
border: 2px solid #eee;
/* 테두리 */
display: flex;
align-items: center;
justify-content: center;
background-color: #f9f9f9;
/* 기본 배경색 */
}
.cropped-profile-img {
width: 100%;
height: 100%;
object-fit: cover;
/* 이미지를 영역에 꽉 채우되 비율 유지 */
}
.default-profile-icon {
font-size: 60px;
color: #ccc;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.remove-btn {
display: inline-block;
/* 버튼 형태로 보이기 위해 */
padding: 10px 20px;
margin: 5px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
}
.file-label.upload-btn:hover,
.remove-btn:hover {
background-color: #0056b3;
}
.remove-btn {
background-color: #dc3545;
}
.remove-btn:hover {
background-color: #c82333;
}
</style>