휴가관리 수정정

This commit is contained in:
dyhj625 2025-02-24 15:29:32 +09:00
parent 09e79cb690
commit efbeee855a
2 changed files with 93 additions and 59 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="card-body d-flex justify-content-center"> <div class="card-body d-flex justify-content-center m-n5">
<ul class="list-unstyled d-flex flex-wrap align-items-center gap-2 mb-0 mt-2"> <ul class="list-unstyled d-flex flex-wrap align-items-center gap-2 mb-0 mt-2">
<li <li
v-for="(user, index) in sortedUserList" v-for="(user, index) in sortedUserList"

View File

@ -5,12 +5,16 @@
<!-- Sidebar: 사이드바 영역 --> <!-- Sidebar: 사이드바 영역 -->
<div class="col-3 app-calendar-sidebar border-end" id="app-calendar-sidebar"> <div class="col-3 app-calendar-sidebar border-end" id="app-calendar-sidebar">
<div class="sidebar-content"> <div class="sidebar-content">
<!-- 사원 프로필 리스트 --> <div class="sidebar-actions text-center my-3">
<HalfDayButtons
@toggleHalfDay="toggleHalfDay"
@addVacationRequests="saveVacationChanges"
/>
</div>
<ProfileList <ProfileList
@profileClick="handleProfileClick" @profileClick="handleProfileClick"
:remainingVacationData="remainingVacationData" :remainingVacationData="remainingVacationData"
/> />
<!-- 모달들은 화면 오버레이로 동작하므로 사이드바 내부에 두어도 무방 -->
<VacationModal <VacationModal
v-if="isModalOpen" v-if="isModalOpen"
:isOpen="isModalOpen" :isOpen="isModalOpen"
@ -28,24 +32,23 @@
@updateVacation="fetchRemainingVacation" @updateVacation="fetchRemainingVacation"
/> />
</div> </div>
<div class="sidebar-actions text-center my-3">
<!-- 액션 버튼 -->
<HalfDayButtons
@toggleHalfDay="toggleHalfDay"
@addVacationRequests="saveVacationChanges"
/>
</div>
</div> </div>
<!-- Main Content: 캘린더 영역 --> <!-- Main Content: 캘린더 영역 -->
<div class="col app-calendar-content"> <div class="col app-calendar-content">
<div class="card shadow-none border-0"> <div class="card shadow-none border-0">
<div class="card-body pb-0"> <div class="card-body pb-0" style="position: relative;">
<full-calendar <full-calendar
ref="fullCalendarRef" ref="fullCalendarRef"
:options="calendarOptions" :options="calendarOptions"
class="flatpickr-calendar-only" class="flatpickr-calendar-only"
/> />
<!-- 숨겨진 데이트피커 인풋 -->
<input
ref="calendarDatepicker"
type="text"
style="display: none; position: absolute;"
/>
</div> </div>
</div> </div>
</div> </div>
@ -60,7 +63,12 @@
import FullCalendar from "@fullcalendar/vue3"; import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid"; import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
// Flatpickr MonthSelect
import flatpickr from "flatpickr";
import monthSelectPlugin from "flatpickr/dist/plugins/monthSelect/index";
import "flatpickr/dist/flatpickr.min.css"; import "flatpickr/dist/flatpickr.min.css";
import "flatpickr/dist/plugins/monthSelect/style.css";
import "@/assets/css/app-calendar.css"; import "@/assets/css/app-calendar.css";
import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap-icons/font/bootstrap-icons.css";
import HalfDayButtons from "@c/button/HalfDayButtons.vue"; import HalfDayButtons from "@c/button/HalfDayButtons.vue";
@ -75,11 +83,11 @@
const userListStore = useUserStore(); const userListStore = useUserStore();
const userList = ref([]); const userList = ref([]);
const userColors = ref({}); const userColors = ref({});
const myVacations = ref([]); // " " ( ) const myVacations = ref([]); //
const receivedVacations = ref([]); // " " const receivedVacations = ref([]);
const isModalOpen = ref(false); const isModalOpen = ref(false);
const remainingVacationData = ref({}); const remainingVacationData = ref({});
const modalYear = ref(new Date().getFullYear());
const lastRemainingYear = ref(new Date().getFullYear()); const lastRemainingYear = ref(new Date().getFullYear());
const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0")); const lastRemainingMonth = ref(String(new Date().getMonth() + 1).padStart(2, "0"));
const isGrantModalOpen = ref(false); const isGrantModalOpen = ref(false);
@ -94,8 +102,9 @@
const holidayDates = ref(new Set()); const holidayDates = ref(new Set());
const fetchedEvents = ref([]); const fetchedEvents = ref([]);
// : ( ) // ref
const toggledDates = ref(new Set()); const calendarDatepicker = ref(null);
let fpInstance = null;
const calendarOptions = reactive({ const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin], plugins: [dayGridPlugin, interactionPlugin],
@ -115,9 +124,51 @@
onMounted(async () => { onMounted(async () => {
await userStore.userInfo(); await userStore.userInfo();
await fetchRemainingVacation(); await fetchRemainingVacation();
// vacation history
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
await fetchVacationHistory(currentYear); await fetchVacationHistory(currentYear);
// Flatpickr ( )
fpInstance = flatpickr(calendarDatepicker.value, {
dateFormat: "Y-m",
plugins: [
new monthSelectPlugin({
shorthand: true,
dateFormat: "Y-m",
altFormat: "F Y"
})
],
onChange: function(selectedDatesArr, dateStr) {
//
fullCalendarRef.value.getApi().gotoDate(dateStr + "-01");
const [year, month] = dateStr.split("-");
lastRemainingYear.value = parseInt(year, 10);
lastRemainingMonth.value = month;
loadCalendarData(lastRemainingYear.value, lastRemainingMonth.value);
},
onClose: function() {
calendarDatepicker.value.style.display = "none";
}
});
// FullCalendar (.fc-toolbar-title)
nextTick(() => {
const titleEl = document.querySelector('.fc-toolbar-title');
if (titleEl) {
titleEl.style.cursor = 'pointer';
titleEl.addEventListener('click', () => {
//
const rect = titleEl.getBoundingClientRect();
const dpEl = calendarDatepicker.value;
dpEl.style.display = 'none';
dpEl.style.position = 'absolute';
dpEl.style.top = (rect.bottom + window.scrollY -120) + 'px';
// dpEl.style.left = (rect.left) + 'px';
dpEl.style.width = '0px'; //
dpEl.style.fontSize = '0.9em';
fpInstance.open();
});
}
});
}); });
// API ( ) // API ( )
@ -137,7 +188,6 @@
} }
} }
// lastRemainingYear
watch(lastRemainingYear, async (newYear, oldYear) => { watch(lastRemainingYear, async (newYear, oldYear) => {
await fetchVacationHistory(newYear); await fetchVacationHistory(newYear);
}); });
@ -156,27 +206,18 @@
} }
}; };
//
const handleProfileClick = async (user) => { const handleProfileClick = async (user) => {
try { try {
if (isModalOpen.value) { //
isModalOpen.value = false; isModalOpen.value = false;
return;
}
if (isGrantModalOpen.value) {
isGrantModalOpen.value = false; isGrantModalOpen.value = false;
return;
}
if (user.MEMBERSEQ === userStore.user.id) { if (user.MEMBERSEQ === userStore.user.id) {
const year = new Date().getFullYear(); const displayedYear = lastRemainingYear.value;
await fetchVacationHistory(year); await fetchVacationHistory(displayedYear);
isModalOpen.value = true; isModalOpen.value = true;
lastRemainingYear.value = year;
isGrantModalOpen.value = false;
} else { } else {
selectedUser.value = user; selectedUser.value = user;
isGrantModalOpen.value = true; isGrantModalOpen.value = true;
isModalOpen.value = false;
} }
} catch (error) { } catch (error) {
console.error("🚨 연차 데이터 불러오기 실패:", error); console.error("🚨 연차 데이터 불러오기 실패:", error);
@ -220,29 +261,23 @@
return vacationCodeMap.value[typeCode] || "기타"; return vacationCodeMap.value[typeCode] || "기타";
}; };
// computed: lastRemainingYear
const filteredMyVacations = computed(() => { const filteredMyVacations = computed(() => {
const filtered = myVacations.value.filter(vac => { return myVacations.value.filter(vac => {
const dateStr = vac.date || vac.LOCVACUDT; const dateStr = vac.date;
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null; const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
return year === String(lastRemainingYear.value); return year === String(lastRemainingYear.value);
}); });
console.log("filteredMyVacations:", filtered);
return filtered;
}); });
const filteredReceivedVacations = computed(() => { const filteredReceivedVacations = computed(() => {
return receivedVacations.value.filter(vac => { return receivedVacations.value.filter(vac => {
const dateStr = vac.date || vac.LOCVACUDT; const dateStr = vac.date;
const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null; const year = dateStr ? dateStr.split("T")[0].substring(0, 4) : null;
console.log("vacation year:", year, "lastRemainingYear:", lastRemainingYear.value);
return dateStr && year === String(lastRemainingYear.value); return dateStr && year === String(lastRemainingYear.value);
}); });
}); });
function updateCalendarEvents() { function updateCalendarEvents() {
// selectedDates "delete"
const selectedEvents = Array.from(selectedDates.value) const selectedEvents = Array.from(selectedDates.value)
.filter(([date, type]) => type !== "delete") .filter(([date, type]) => type !== "delete")
.map(([date, type]) => ({ .map(([date, type]) => ({
@ -254,8 +289,6 @@
classNames: [getVacationTypeClass(type), "selected-event"] classNames: [getVacationTypeClass(type), "selected-event"]
})); }));
// fetchedEvents, "delete"
// () , .
const filteredFetchedEvents = fetchedEvents.value.filter(event => { const filteredFetchedEvents = fetchedEvents.value.filter(event => {
if (event.saved && selectedDates.value.get(event.start) === "delete") { if (event.saved && selectedDates.value.get(event.start) === "delete") {
if (event.memberSeq === userStore.user.id) { if (event.memberSeq === userStore.user.id) {
@ -287,15 +320,12 @@
) { ) {
return; return;
} }
// : LOCVACUDT 10 clickedDateStr , LOCVACRMM
const isMyVacation = myVacations.value.some(vac => { const isMyVacation = myVacations.value.some(vac => {
const vacDate = vac.date ? String(vac.date).substring(0, 10) : ""; const vacDate = vac.date ? String(vac.date).substring(0, 10) : "";
return vacDate === clickedDateStr && return vacDate === clickedDateStr && !vac.receiverId;
(!vac.LOCVACRMM || String(vac.LOCVACRMM).trim() === "");
}); });
if (isMyVacation) { if (isMyVacation) {
// : selectedDates "delete" ( )
if (selectedDates.value.get(clickedDateStr) === "delete") { if (selectedDates.value.get(clickedDateStr) === "delete") {
selectedDates.value.delete(clickedDateStr); selectedDates.value.delete(clickedDateStr);
} else { } else {
@ -305,9 +335,7 @@
return; return;
} }
// : /
if (selectedDates.value.has(clickedDateStr)) { if (selectedDates.value.has(clickedDateStr)) {
console.log("일반 날짜 토글 off: 기존 선택 해제");
selectedDates.value.delete(clickedDateStr); selectedDates.value.delete(clickedDateStr);
updateCalendarEvents(); updateCalendarEvents();
return; return;
@ -315,7 +343,6 @@
const type = halfDayType.value const type = halfDayType.value
? (halfDayType.value === "AM" ? "700101" : "700102") ? (halfDayType.value === "AM" ? "700101" : "700102")
: "700103"; : "700103";
console.log("일반 날짜 토글 on: 선택 및 타입", type);
selectedDates.value.set(clickedDateStr, type); selectedDates.value.set(clickedDateStr, type);
halfDayType.value = null; halfDayType.value = null;
updateCalendarEvents(); updateCalendarEvents();
@ -331,7 +358,6 @@
if (response.status === 200) { if (response.status === 200) {
const vacationList = response.data; const vacationList = response.data;
if (lastRemainingYear.value !== year) { if (lastRemainingYear.value !== year) {
//
myVacations.value = vacationList.filter( myVacations.value = vacationList.filter(
(vac) => vac.MEMBERSEQ === userStore.user.id (vac) => vac.MEMBERSEQ === userStore.user.id
); );
@ -348,7 +374,7 @@
backgroundColor, backgroundColor,
classNames: [getVacationTypeClass(vac.LOCVACTYP)], classNames: [getVacationTypeClass(vac.LOCVACTYP)],
saved: true, saved: true,
memberSeq: vac.MEMBERSEQ, // memberSeq: vac.MEMBERSEQ,
}; };
}) })
.filter((event) => event.start); .filter((event) => event.start);
@ -368,18 +394,18 @@
const vacationsToAdd = selectedDatesArray const vacationsToAdd = selectedDatesArray
.filter(([date, type]) => type !== "delete") .filter(([date, type]) => type !== "delete")
.filter(([date, type]) => .filter(([date, type]) =>
!myVacations.value.some(vac => vac.LOCVACUDT && vac.LOCVACUDT.startsWith(date)) || !myVacations.value.some(vac => vac.date && vac.date.startsWith(date)) ||
myVacations.value.some(vac => vac.LOCVACUDT && vac.LOCVACUDT.startsWith(date) && vac.LOCVACRMM) myVacations.value.some(vac => vac.date && vac.date.startsWith(date) && vac.receiverId)
) )
.map(([date, type]) => ({ date, type })); .map(([date, type]) => ({ date, type }));
const vacationsToDelete = myVacations.value const vacationsToDelete = myVacations.value
.filter(vac => { .filter(vac => {
if (!vac.LOCVACUDT) return false; if (!vac.date) return false;
const date = vac.LOCVACUDT.split("T")[0]; const date = vac.date.split("T")[0];
return selectedDates.value.get(date) === "delete" && !vac.LOCVACRMM; return selectedDates.value.get(date) === "delete" && !vac.receiverId;
}) })
.map(vac => { .map(vac => {
const id = vac.LOCVACSEQ; const id = vac.id;
return typeof id === "number" ? Number(id) : id; return typeof id === "number" ? Number(id) : id;
}); });
try { try {
@ -390,6 +416,9 @@
if (response.data && response.data.status === "OK") { if (response.data && response.data.status === "OK") {
alert("✅ 휴가 변경 사항이 저장되었습니다."); alert("✅ 휴가 변경 사항이 저장되었습니다.");
await fetchRemainingVacation(); await fetchRemainingVacation();
if (isModalOpen.value) {
await fetchVacationHistory(lastRemainingYear.value);
}
const currentDate = fullCalendarRef.value.getApi().getDate(); const currentDate = fullCalendarRef.value.getApi().getDate();
await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1); await loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
selectedDates.value.clear(); selectedDates.value.clear();
@ -440,5 +469,10 @@
</script> </script>
<style> <style>
/* 스타일 정의 */ /* 기본 스타일은 그대로 두고, 데이트피커 인풋의 추가 스타일 정의 */
.fc-toolbar-title {
cursor: pointer;
}
/* 데이트피커 인풋은 Flatpickr에서 동적으로 스타일 적용됨 */
</style> </style>