휴가관리 수정

This commit is contained in:
dyhj625 2025-02-24 12:08:02 +09:00
parent fe00b53e4f
commit f2b364f4f8
4 changed files with 141 additions and 90 deletions

View File

@ -5,7 +5,7 @@
<button class="close-btn" @click="closeModal"></button>
<div class="modal-body">
<p>해당 직원에게 부여연차 개수를 선택하세요. (남은 개수: {{ availableQuota }})</p>
<p>선물연차 개수를 선택하세요.</p>
<div class="vacation-control">
<button @click="decreaseCount" :disabled="grantCount < 2" class="count-btn">-</button>
@ -129,7 +129,7 @@
background: white;
padding: 20px;
border-radius: 12px;
width: 400px;
width: 300px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
text-align: center;
position: relative;

View File

@ -151,7 +151,7 @@ const closeModal = () => {
background: white;
padding: 20px;
border-radius: 12px;
width: 400px;
width: 300px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
text-align: center;
position: relative;
@ -222,4 +222,10 @@ const closeModal = () => {
color: gray;
margin-top: 10px;
}
/* 모달 본문 스크롤: 높이가 300px 이상이면 스크롤바 표시 */
.modal-body {
max-height: 130px;
overflow-y: auto;
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="card-body d-flex justify-content-center">
<ul class="list-unstyled d-flex align-items-center gap-5 mb-0 mt-2">
<ul class="list-unstyled d-flex flex-wrap align-items-center gap-2 mb-0 mt-2">
<li
v-for="(user, index) in sortedUserList"
:key="index"
@ -34,8 +34,8 @@
defineEmits(["profileClick"]);
defineProps({
remainingVacationData: Object,
});
remainingVacationData: Object,
});
const userStore = useUserInfoStore();
const userListStore = useUserStore();
@ -47,28 +47,28 @@
const userColors = ref({});
onMounted(async () => {
await userStore.userInfo();
if (userStore.user) {
employeeId.value = userStore.user.id;
} else {
console.error("❌ 로그인한 사용자 정보를 불러오지 못했습니다.");
}
await userStore.userInfo();
if (userStore.user) {
employeeId.value = userStore.user.id;
} else {
console.error("❌ 로그인한 사용자 정보를 불러오지 못했습니다.");
}
await userListStore.fetchUserList();
userList.value = userListStore.userList;
await userListStore.fetchUserList();
userList.value = userListStore.userList;
//
userList.value.forEach(user => {
userColors.value[user.MEMBERSEQ] = user.usercolor || "#ccc";
});
//
userList.value.forEach(user => {
userColors.value[user.MEMBERSEQ] = user.usercolor || "#ccc";
});
nextTick(() => {
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltips.forEach(tooltip => {
new bootstrap.Tooltip(tooltip);
nextTick(() => {
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltips.forEach(tooltip => {
new bootstrap.Tooltip(tooltip);
});
});
});
});
const sortedUserList = computed(() => {
if (!employeeId.value) return userList.value; //
@ -78,6 +78,7 @@
return myProfile ? [myProfile, ...otherUsers] : userList.value;
});
// URL
const getUserProfileImage = (profilePath) => {
return profilePath && profilePath.trim()
? `${baseUrl}upload/img/profile/${profilePath}`
@ -92,14 +93,12 @@
event.target.style.visibility = "visible";
};
//
// :
const profileSize = computed(() => {
const totalUsers = userList.value.length;
if (totalUsers <= 7) return "100px"; // 7
if (totalUsers <= 10) return "80px"; // ~10
if (totalUsers <= 20) return "60px"; // ~20
return "40px"; // 20
if (totalUsers <= 10) return "68px"; // ~10
if (totalUsers <= 15) return "50px"; // ~20
return "30px"; // 20
});
//
@ -109,18 +108,29 @@
height: profileSize.value,
borderWidth: "4px",
borderColor: user.usercolor || "#ccc",
borderStyle: "solid",
borderStyle: "solid",
};
};
</script>
<style scoped>
/* 남은 연차 개수 스타일 */
.remaining-vacation {
display: block;
text-align: center;
font-size: 14px;
color: #333;
margin-top: 5px;
}
.remaining-vacation {
display: block;
text-align: center;
font-size: 14px;
color: #333;
margin-top: 5px;
}
/* ul에 flex-wrap을 적용하여 넘치는 프로필이 다음 줄로 내려가도록 함 */
ul {
flex-wrap: wrap;
justify-content: center;
}
/* li 간 간격 조정 */
li {
margin: 5px;
}
</style>

View File

@ -1,43 +1,51 @@
<template>
<div class="vacation-management">
<div class="container-xxl flex-grow-1 container-p-y">
<div class="card app-calendar-wrapper">
<div class="row g-0">
<div class="col app-calendar-content">
<div class="card shadow-none border-0">
<ProfileList
@profileClick="handleProfileClick"
:remainingVacationData="remainingVacationData"
/>
<div class="card-body w-75 p-3 align-self-center">
<!-- 모달에 필터링된 연차 목록 전달 -->
<VacationModal
v-if="isModalOpen"
:isOpen="isModalOpen"
:myVacations="filteredMyVacations"
:receivedVacations="filteredReceivedVacations"
:userColors="userColors"
@close="isModalOpen = false"
/>
<div class="container-xxl flex-grow-1 container-p-y">
<div class="card app-calendar-wrapper">
<div class="row g-0">
<!-- Sidebar: 사이드바 영역 -->
<div class="col-3 app-calendar-sidebar border-end" id="app-calendar-sidebar">
<!-- 모달들은 화면 오버레이로 동작하므로 사이드바 내부에 두어도 무방 -->
<VacationModal
v-if="isModalOpen"
:isOpen="isModalOpen"
:myVacations="filteredMyVacations"
:receivedVacations="filteredReceivedVacations"
:userColors="userColors"
@close="isModalOpen = false"
/>
<VacationGrantModal
v-if="isGrantModalOpen"
:isOpen="isGrantModalOpen"
:targetUser="selectedUser"
:remainingQuota="remainingVacationData[selectedUser?.MEMBERSEQ] || 0"
@close="isGrantModalOpen = false"
@updateVacation="fetchRemainingVacation"
/>
<div class="sidebar-content">
<!-- 사원 프로필 리스트 -->
<ProfileList
@profileClick="handleProfileClick"
:remainingVacationData="remainingVacationData"
/>
</div>
<div class="sidebar-actions text-center my-3">
<!-- 액션 버튼 -->
<HalfDayButtons
@toggleHalfDay="toggleHalfDay"
@addVacationRequests="saveVacationChanges"
/>
</div>
</div>
<VacationGrantModal
v-if="isGrantModalOpen"
:isOpen="isGrantModalOpen"
:targetUser="selectedUser"
:remainingQuota="remainingVacationData[selectedUser?.MEMBERSEQ] || 0"
@close="isGrantModalOpen = false"
@updateVacation="fetchRemainingVacation"
/>
<full-calendar
ref="fullCalendarRef"
:options="calendarOptions"
class="flatpickr-calendar-only"
/>
<HalfDayButtons
@toggleHalfDay="toggleHalfDay"
@addVacationRequests="saveVacationChanges"
/>
</div>
<!-- Main Content: 캘린더 영역 -->
<div class="col app-calendar-content">
<div class="card shadow-none border-0">
<div class="card-body pb-0">
<full-calendar
ref="fullCalendarRef"
:options="calendarOptions"
class="flatpickr-calendar-only"
/>
</div>
</div>
</div>
@ -46,8 +54,9 @@
</div>
</template>
<script setup>
import { reactive, ref, onMounted, nextTick, computed } from "vue";
import { reactive, ref, onMounted, nextTick, computed, watch } from "vue";
import axios from "@api";
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
@ -71,11 +80,9 @@
const receivedVacations = ref([]); // " "
const isModalOpen = ref(false);
const remainingVacationData = ref({});
// ( )
const modalYear = ref(new Date().getFullYear());
const modalMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
const lastRemainingYear = ref(new Date().getFullYear());
const lastRemainingYear = ref(new Date().getFullYear());
const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
const isGrantModalOpen = ref(false);
const selectedUser = ref(null);
@ -108,6 +115,23 @@
await fetchRemainingVacation();
});
// lastRemainingYear
watch(lastRemainingYear, async (newYear, oldYear) => {
try {
const response = await axios.get(`vacation/history?year=${newYear}`);
if (response.status === 200 && response.data) {
myVacations.value = response.data.data.usedVacations || [];
receivedVacations.value = response.data.data.receivedVacations || [];
} else {
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
myVacations.value = [];
receivedVacations.value = [];
}
} catch (error) {
console.error("🚨 연차 데이터 불러오기 실패:", error);
}
});
const fetchRemainingVacation = async () => {
try {
const response = await axios.get("vacation/remaining");
@ -126,6 +150,11 @@
//
const handleProfileClick = async (user) => {
try {
//
if (isModalOpen.value) {
isModalOpen.value = false;
return;
}
if (user.MEMBERSEQ === userStore.user.id) {
const year = new Date().getFullYear(); //
//
@ -135,12 +164,17 @@ const handleProfileClick = async (user) => {
receivedVacations.value = response.data.data.receivedVacations || [];
isModalOpen.value = true;
//
modalYear.value = year;
lastRemainingYear.value = year;
isGrantModalOpen.value = false;
} else {
console.warn("❌ 연차 내역을 불러오지 못했습니다.");
}
} else {
//
if (isGrantModalOpen.value) {
isGrantModalOpen.value = false;
return;
}
selectedUser.value = user;
isGrantModalOpen.value = true;
isModalOpen.value = false;
@ -187,14 +221,14 @@ const handleProfileClick = async (user) => {
return vacationCodeMap.value[typeCode] || "기타";
};
// computed: modalYear
// computed: lastRemainingYear
const filteredMyVacations = computed(() => {
const filtered = myVacations.value.filter(vac => {
// vac.date vac.LOCVACUDT
const dateStr = vac.date || vac.LOCVACUDT;
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
console.log("vacation year:", year, "modalYear:", modalYear.value);
return year === String(modalYear.value);
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
return year === String(lastRemainingYear.value);
});
console.log("filteredMyVacations:", filtered);
return filtered;
@ -204,8 +238,8 @@ const filteredReceivedVacations = computed(() => {
return receivedVacations.value.filter(vac => {
const dateStr = vac.date || vac.LOCVACUDT;
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
console.log("vacation year:", year, "modalYear:", modalYear.value);
return dateStr && year === String(modalYear.value);
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
return dateStr && year === String(lastRemainingYear.value);
});
});
@ -277,12 +311,12 @@ const filteredReceivedVacations = computed(() => {
const response = await axios.get(`vacation/list/${year}/${month}`);
if (response.status === 200) {
const vacationList = response.data;
// modalYear
if (modalYear.value !== year) {
// lastRemainingYear
if (lastRemainingYear.value !== year) {
myVacations.value = vacationList.filter(
(vac) => vac.MEMBERSEQ === userStore.user.id
);
modalYear.value = year;
lastRemainingYear.value = year;
// modalMonth ( )
}
//
@ -359,9 +393,10 @@ const filteredReceivedVacations = computed(() => {
}
async function loadCalendarData(year, month) {
if (lastRemainingYear.value !== year) {
if (lastRemainingYear.value !== year || lastRemainingMonth.value !== month) {
await fetchRemainingVacation();
lastRemainingYear.value = year;
lastRemainingMonth.value = month;
}
fetchedEvents.value = [];
const [vacationEvents, holidayEvents] = await Promise.all([