휴가저장장

This commit is contained in:
dyhj625 2025-02-11 15:03:25 +09:00
parent 724f0afc61
commit 4daab961cf
6 changed files with 492 additions and 359 deletions

View File

@ -0,0 +1,43 @@
<template>
<div class="half-day-buttons">
<button
class="btn btn-info"
:class="{ active: halfDayType === 'AM' }"
@click="toggleHalfDay('AM')"
>
<i class="bi bi-sun"></i>
</button>
<button
class="btn btn-warning"
:class="{ active: halfDayType === 'PM' }"
@click="toggleHalfDay('PM')"
>
<i class="bi bi-moon"></i>
</button>
<div class="save-button-container">
<button class="btn btn-success" @click="addVacationRequests">
</button>
</div>
</div>
</template>
<script setup>
import { defineEmits, ref } from "vue";
const emit = defineEmits(["toggleHalfDay", "addVacationRequests"]);
const halfDayType = ref(null);
const toggleHalfDay = (type) => {
halfDayType.value = halfDayType.value === type ? null : type;
emit("toggleHalfDay", halfDayType.value);
};
const addVacationRequests = () => {
emit("addVacationRequests");
};
</script>
<style scoped>
</style>

View File

@ -0,0 +1,187 @@
<template>
<div class="users-list-wrapper">
<button class="scroll-btn left" @click="scrollLeft"></button>
<div class="users-list-container" ref="userListContainer">
<ul class="list-unstyled users-list d-flex align-items-center justify-content-start">
<li
v-for="(user, index) in userList"
:key="index"
class="avatar pull-up position-relative"
:class="{ disabled: user.disabled }"
@click="toggleDisable(index)"
data-bs-toggle="tooltip"
data-popup="tooltip-custom"
data-bs-placement="top"
:aria-label="user.MEMBERSEQ"
:data-bs-original-title="getTooltipTitle(user)"
>
<div class="tooltip-custom">{{ getTooltipTitle(user) }}</div>
<img
class="rounded-circle user-avatar"
:src="getUserProfileImage(user.MEMBERPRF)"
alt="user"
:style="{ borderColor: user.usercolor }"
@error="setDefaultImage"
@load="showImage"
/>
</li>
</ul>
</div>
<button class="scroll-btn right" @click="scrollRight"></button>
</div>
</template>
<script setup>
import { onMounted, ref, nextTick } from "vue";
import { useUserStore } from "@s/userList";
import $api from "@api";
const emit = defineEmits();
const userStore = useUserStore();
const userList = ref([]);
const userListContainer = ref(null); //
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, "");
const defaultProfile = "/img/icons/icon.png"; //
//
onMounted(async () => {
await userStore.fetchUserList();
userList.value = userStore.userList;
nextTick(() => {
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltips.forEach((tooltip) => {
new bootstrap.Tooltip(tooltip);
});
});
});
//
const getUserProfileImage = (profilePath) => {
return profilePath && profilePath.trim()
? `${baseUrl}upload/img/profile/${profilePath}`
: defaultProfile;
};
//
const setDefaultImage = (event) => {
event.target.src = defaultProfile;
};
//
const showImage = (event) => {
event.target.style.visibility = "visible";
};
//
const getTooltipTitle = (user) => {
return user.MEMBERSEQ === userList.value.MEMBERSEQ ? "나" : user.MEMBERNAM;
};
// ( )
const scrollLeft = () => {
userListContainer.value.scrollBy({ left: -userListContainer.value.clientWidth, behavior: "smooth" });
};
const scrollRight = () => {
userListContainer.value.scrollBy({ left: userListContainer.value.clientWidth, behavior: "smooth" });
};
</script>
<style scoped>
/* ✅ 전체 프로필 목록 감싸는 영역 */
.users-list-wrapper {
position: relative;
width: 100%;
max-width: 1250px;
margin: auto;
}
/* ✅ 스크롤 가능하도록 컨테이너 설정 */
.users-list-container {
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
display: flex;
flex-wrap: wrap;
max-height: 200px; /* 한 줄 이상 넘어가지 않도록 조절 */
padding: 50px;
scrollbar-width: none; /* Firefox에서 스크롤바 숨김 */
}
/* ✅ 스크롤바 숨기기 */
.users-list-container::-webkit-scrollbar {
display: none; /* Chrome, Safari에서 스크롤바 숨김 */
}
/* ✅ 프로필 목록 */
.users-list {
display: flex;
flex-wrap: wrap;
gap: 20px; /* 프로필 간격 */
padding: 10px;
}
/* ✅ 프로필 이미지 스타일 */
.user-avatar {
border: 3px solid;
width: 100px;
height: 100px;
object-fit: cover;
background-color: white;
transition: transform 0.2s ease-in-out;
}
/* ✅ 프로필 선택 효과 */
.avatar:hover .user-avatar {
transform: scale(1.1);
}
/* ✅ 툴팁 위치 조정 */
.tooltip-custom {
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.75);
color: white;
padding: 5px 8px;
font-size: 12px;
border-radius: 5px;
white-space: nowrap;
display: none;
}
/* ✅ 툴팁 표시 */
.avatar:hover .tooltip-custom {
display: block;
}
/* ✅ 좌우 스크롤 버튼 */
.scroll-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background-color: rgba(0, 0, 0, 0.5);
color: white;
border: none;
cursor: pointer;
padding: 10px 15px;
font-size: 20px;
z-index: 100;
transition: background 0.3s ease;
}
.scroll-btn.left {
left: -40px;
}
.scroll-btn.right {
right: -40px;
}
.scroll-btn:hover {
background-color: rgba(0, 0, 0, 0.8);
}
</style>

View File

@ -16,7 +16,6 @@ export const useUserStore = defineStore("userStore", {
actions: { actions: {
async fetchUserList() { async fetchUserList() {
const response = await axios.get('user/allUserList'); const response = await axios.get('user/allUserList');
console.log('response',response)
this.userList = response.data.data.allUserList; this.userList = response.data.data.allUserList;
this.userInfo = response.data.data.user; this.userInfo = response.data.data.user;

View File

@ -1,37 +0,0 @@
<template>
<div class="modal">
<div class="modal-content">
<h3>휴가 추가</h3>
<input type="text" v-model="title" placeholder="제목" />
<input type="date" v-model="date" />
<button @click="addEvent">추가</button>
<button @click="$emit('close')">닫기</button>
</div>
</div>
</template>
<script>
import calendarStore from '@s/calendarStore';
export default {
data() {
return {
title: '',
date: '',
};
},
methods: {
addEvent() {
if (this.title && this.date) {
calendarStore.addEvent({ title: this.title, start: this.date });
this.$emit('close');
} else {
alert('모든 필드를 입력해주세요.');
}
},
},
};
</script>
<style scoped>
</style>

View File

@ -1,56 +0,0 @@
<template>
<div class="profile-list">
<div v-for="profile in profiles" :key="profile.id" class="profile">
<img :src="profile.avatar" alt="프로필 사진" class="avatar" />
<div class="info">
<p class="name">{{ profile.name }}</p>
<p class="vacation-count">남은 휴가: {{ profile.remainingVacations }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
profiles: [
{ id: 1, name: '김철수', avatar: '/avatars/user1.png', remainingVacations: 15 },
{ id: 2, name: '박영희', avatar: '/avatars/user2.png', remainingVacations: 11 },
{ id: 3, name: '이민호', avatar: '/avatars/user3.png', remainingVacations: 10 },
],
};
},
};
</script>
<style scoped>
.profile-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.profile {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 5px;
padding: 10px;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 10px;
}
.info {
display: flex;
flex-direction: column;
}
.name {
font-weight: bold;
}
.vacation-count {
color: gray;
}
</style>

View File

@ -1,301 +1,298 @@
<template> <template>
<div class="vacation-management"> <div class="vacation-management">
<div class="container flex-grow-1"> <div class="container flex-grow-1">
<div class="card app-calendar-wrapper"> <div class="card app-calendar-wrapper">
<div class="row g-0"> <div class="row g-0">
<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"> <div class="card-body">
<full-calendar <full-calendar
ref="fullCalendarRef" ref="fullCalendarRef"
:options="calendarOptions" :options="calendarOptions"
class="flatpickr-calendar-only" class="flatpickr-calendar-only"
/> />
</div> </div>
</div>
<div class="half-day-buttons">
<button
class="btn btn-info"
:class="{ active: halfDayType === 'AM' }"
@click="toggleHalfDay('AM')"
>
<i class="bi bi-sun"></i>
</button>
<button
class="btn btn-warning"
:class="{ active: halfDayType === 'PM' }"
@click="toggleHalfDay('PM')"
>
<i class="bi bi-moon"></i>
</button>
<div class="save-button-container">
<button class="btn btn-success" @click="addVacationRequests">
</button>
</div> </div>
</div>
<br /> <ProfileList />
<HalfDayButtons
@toggleHalfDay="toggleHalfDay"
@addVacationRequests="addVacationRequests"
/>
<br />
</div>
</div> </div>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
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";
import "flatpickr/dist/flatpickr.min.css"; import "flatpickr/dist/flatpickr.min.css";
import "@/assets/css/app-calendar.css"; import "@/assets/css/app-calendar.css";
import { reactive, ref, onMounted, nextTick } from "vue"; import { reactive, ref, onMounted, nextTick, watchEffect } from "vue";
import axios from "@api"; import axios from "@api";
import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap-icons/font/bootstrap-icons.css";
import HalfDayButtons from "@c/button/HalfDayButtons.vue";
import ProfileList from "@/components/vacation/ProfileList.vue";
import { useUserStore } from "@s/userList";
// FullCalendar const userStore = useUserStore();
const fullCalendarRef = ref(null); const userList = ref([]);
const calendarEvents = ref([]); // FullCalendar (API + ) const userColors = ref({});
const fetchedEvents = ref([]); // API (, )
const selectedDates = ref(new Map()); //
const halfDayType = ref(null);
const employeeId = ref(1);
// (YYYY-MM-DD ) ( )
const holidayDates = ref(new Set());
// FullCalendar (events calendarEvents ) const fetchUserList = async () => {
const calendarOptions = reactive({ try {
plugins: [dayGridPlugin, interactionPlugin], await userStore.fetchUserList();
initialView: "dayGridMonth", userList.value = userStore.userList;
headerToolbar: {
left: "today",
center: "title",
right: "prev,next",
},
locale: "ko",
selectable: false,
dateClick: handleDateClick,
datesSet: handleMonthChange,
events: calendarEvents,
});
/** if (!userList.value.length) {
* API 이벤트(fetchedEvents) 사용자가 선택한 날짜(selectedDates) 병합하여 console.warn("📌 사용자 목록이 비어 있음!");
* calendarEvents를 업데이트하는 함수 return;
* - 선택 이벤트는 display: "background" 옵션을 사용하여 배경으로 표시 }
* - 선택된 타입에 따라 클래스(selected-am, selected-pm, selected-full) 부여함
*/ userColors.value = {};
function updateCalendarEvents() { userList.value.forEach((user) => {
const selectedEvents = Array.from(selectedDates.value).map(([date, type]) => { userColors.value[user.MEMBERSEQ] = user.usercolor || "#FFFFFF";
let className = "";
let title = "";
if (type === "D") {
className = "selected-am"; // :
title = "오전반차 (선택)";
} else if (type === "N") {
className = "selected-pm"; // :
title = "오후반차 (선택)";
} else {
className = "selected-full"; //
title = "연차 (선택)";
}
return {
title,
start: date,
backgroundColor: "rgba(0, 128, 0, 0.3)",
display: "background",
classNames: [className],
};
}); });
calendarEvents.value = [...fetchedEvents.value, ...selectedEvents]; } catch (error) {
console.error("📌 사용자 목록 불러오기 오류:", error);
} }
};
/** // FullCalendar
* 날짜 클릭 이벤트 const fullCalendarRef = ref(null);
* - 주말(, ) 공휴일은 클릭되지 않음 const calendarEvents = ref([]); // FullCalendar (API + )
* - 클릭 해당 날짜를 selectedDates에 추가 또는 제거한 updateCalendarEvents() 호출 const fetchedEvents = ref([]); // API (, )
*/ const selectedDates = ref(new Map()); //
function handleDateClick(info) { const halfDayType = ref(null);
const clickedDateStr = info.dateStr; const employeeId = ref(1);
const clickedDate = info.date;
// (:6, :0) // (YYYY-MM-DD ) ( )
if (clickedDate.getDay() === 0 || clickedDate.getDay() === 6) { const holidayDates = ref(new Set());
return;
} // FullCalendar (events calendarEvents )
// const calendarOptions = reactive({
if (holidayDates.value.has(clickedDateStr)) { plugins: [dayGridPlugin, interactionPlugin],
return; initialView: "dayGridMonth",
} headerToolbar: {
if (!selectedDates.value.has(clickedDateStr)) { left: "today",
const type = halfDayType.value center: "title",
? halfDayType.value === "AM" right: "prev,next",
? "D" },
: "N" locale: "ko",
: "F"; selectable: false,
selectedDates.value.set(clickedDateStr, type); dateClick: handleDateClick,
datesSet: handleMonthChange,
events: calendarEvents,
});
/**
* API 이벤트(fetchedEvents) 사용자가 선택한 날짜(selectedDates) 병합하여
* calendarEvents를 업데이트하는 함수
* - 선택 이벤트는 display: "background" 옵션을 사용하여 배경으로 표시
* - 선택된 타입에 따라 클래스(selected-am, selected-pm, selected-full) 부여함
*/
function updateCalendarEvents() {
const selectedEvents = Array.from(selectedDates.value).map(([date, type]) => {
let className = "";
let title = "";
if (type === "D") {
className = "selected-am"; // :
title = "오전반차 (선택)";
} else if (type === "N") {
className = "selected-pm"; // :
title = "오후반차 (선택)";
} else { } else {
selectedDates.value.delete(clickedDateStr); className = "selected-full"; //
title = "연차 (선택)";
} }
halfDayType.value = null; return {
updateCalendarEvents(); title,
} start: date,
backgroundColor: "rgba(0, 128, 0, 0.3)",
display: "background",
classNames: [className],
};
});
calendarEvents.value = [...fetchedEvents.value, ...selectedEvents];
}
/** /**
* 오전/오후 반차 버튼 토글 * 날짜 클릭 이벤트
*/ * - 주말(, ) 공휴일은 클릭되지 않음
function toggleHalfDay(type) { * - 클릭 해당 날짜를 selectedDates에 추가 또는 제거한 updateCalendarEvents() 호출
halfDayType.value = halfDayType.value === type ? null : type; */
} function handleDateClick(info) {
const clickedDateStr = info.dateStr;
const clickedDate = info.date;
/** // (:6, :0)
* 백엔드에서 휴가 데이터를 가져와 이벤트로 변환 if (clickedDate.getDay() === 0 || clickedDate.getDay() === 6) {
*/ return;
async function fetchVacationData(year, month) { }
try { //
console.log(`📌 휴가 데이터 요청: ${year}-${month}`); if (holidayDates.value.has(clickedDateStr)) {
const response = await axios.get(`vacation/list/${year}/${month}`); return;
if (response.status == 200) { }
const vacationList = response.data; if (!selectedDates.value.has(clickedDateStr)) {
console.log("📌 백엔드 응답 데이터:", vacationList); const type = halfDayType.value
const events = vacationList ? halfDayType.value === "AM"
.map((vac) => { ? "D"
let dateStr = vac.LOCVACUDT.split("T")[0]; : "N"
let className = "fc-daygrid-event"; : "F";
let backgroundColor = getColorByEmployeeId(vac.MEMBERSEQ); selectedDates.value.set(clickedDateStr, type);
let title = "연차"; } else {
if (vac.LOCVACTYP === "D") { selectedDates.value.delete(clickedDateStr);
title = "오전반차"; }
className += " half-day-am"; halfDayType.value = null;
} else if (vac.LOCVACTYP === "N") { updateCalendarEvents();
title = "오후반차"; }
className += " half-day-pm";
} else if (vac.LOCVACTYP === "F") { /**
title = "연차"; * 오전/오후 반차 버튼 토글
className += " full-day"; */
} function toggleHalfDay(type) {
return { halfDayType.value = halfDayType.value === type ? null : type;
title, }
start: dateStr,
backgroundColor, /**
classNames: [className], * 백엔드에서 휴가 데이터를 가져와 이벤트로 변환
}; */
}) async function fetchVacationData(year, month) {
.filter((event) => event !== null); try {
return events; const response = await axios.get(`vacation/list/${year}/${month}`);
} else { if (response.status == 200) {
console.warn("📌 휴가 데이터를 불러오지 못함"); const vacationList = response.data;
return []; const events = vacationList
} .map((vac) => {
} catch (error) { let dateStr = vac.LOCVACUDT.split("T")[0];
console.error("Error fetching vacation data:", error); let className = "fc-daygrid-event";
return []; let backgroundColor = userColors.value[vac.MEMBERSEQ] || "#FFFFFF";
let title = "연차";
if (vac.LOCVACTYP === "D") {
title = "오전반차";
className += " half-day-am";
} else if (vac.LOCVACTYP === "N") {
title = "오후반차";
className += " half-day-pm";
} else if (vac.LOCVACTYP === "F") {
title = "연차";
className += " full-day";
}
return {
title,
start: dateStr,
backgroundColor,
classNames: [className],
};
})
.filter((event) => event !== null);
return events;
} else {
console.warn("📌 휴가 데이터를 불러오지 못함");
return [];
} }
} } catch (error) {
console.error("Error fetching vacation data:", error);
return [];
}
}
/** /**
* 사원 ID별 색상 반환 * 휴가 요청 추가 (선택된 날짜를 백엔드로 전송)
*/ */
function getColorByEmployeeId(employeeId) { async function addVacationRequests() {
const colors = ["#ade3ff", "#ffade3", "#ade3ad", "#ffadad"]; if (selectedDates.value.size === 0) {
return colors[employeeId % colors.length]; alert("휴가를 선택해주세요.");
} return;
}
/** const vacationRequests = Array.from(selectedDates.value).map(([date, type]) => ({
* 휴가 요청 추가 (선택된 날짜를 백엔드로 전송) date,
*/ type,
async function addVacationRequests() { employeeId: employeeId.value,
if (selectedDates.value.size === 0) { }));
alert("휴가를 선택해주세요."); try {
return; const response = await axios.post("vacation", vacationRequests);
} if (response.data && response.data.status === "OK") {
const vacationRequests = Array.from(selectedDates.value).map(([date, type]) => ({ alert("휴가가 저장되었습니다.");
date, //
type, const currentDate = fullCalendarRef.value.getApi().getDate();
employeeId: employeeId.value,
}));
try {
const response = await axios.post("vacation", vacationRequests);
if (response.data && response.data.status === "OK") {
alert("휴가가 저장되었습니다.");
//
const currentDate = fullCalendarRef.value.getApi().getDate();
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
loadCalendarData(year, month);
selectedDates.value.clear();
updateCalendarEvents();
} else {
alert("휴가 저장 중 오류가 발생했습니다.");
}
} catch (error) {
console.error(error);
alert("휴가 저장에 실패했습니다.");
}
}
/**
* 공휴일 데이터 요청 이벤트 변환
*/
async function fetchHolidays(year, month) {
try {
console.log(`📌 공휴일 요청: ${year}-${month}`);
const response = await axios.get(`vacation/${year}/${month}`);
console.log("📌 공휴일 API 응답:", response.data);
const holidayEvents = response.data.map((holiday) => ({
title: holiday.name,
start: holiday.date, // "YYYY-MM-DD"
backgroundColor: "#ff6666",
classNames: ["holiday-event"],
}));
return holidayEvents;
} catch (error) {
console.error("공휴일 정보를 불러오지 못했습니다.", error);
return [];
}
}
/**
* 달력 변경 호출 (FullCalendar의 datesSet 옵션)
*/
function handleMonthChange(viewInfo) {
const currentDate = viewInfo.view.currentStart;
const year = currentDate.getFullYear(); const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, "0"); const month = String(currentDate.getMonth() + 1).padStart(2, "0");
console.log(`📌 월 변경 감지: ${year}-${month}`);
loadCalendarData(year, month); loadCalendarData(year, month);
} selectedDates.value.clear();
/**
* 지정한 월의 데이터를 로드 (휴가, 공휴일 데이터를 병렬 요청)
*/
async function loadCalendarData(year, month) {
console.log(`📌 ${year}-${month} 데이터 로드 시작`);
fetchedEvents.value = [];
const [vacationEvents, holidayEvents] = await Promise.all([
fetchVacationData(year, month),
fetchHolidays(year, month),
]);
console.log("📌 변환된 휴가 이벤트:", vacationEvents);
console.log("📌 변환된 공휴일 이벤트:", holidayEvents);
// Set
holidayDates.value = new Set(holidayEvents.map((event) => event.start));
fetchedEvents.value = [...vacationEvents, ...holidayEvents];
updateCalendarEvents(); updateCalendarEvents();
await nextTick(); } else {
fullCalendarRef.value.getApi().refetchEvents(); alert("휴가 저장 중 오류가 발생했습니다.");
console.log("📌 FullCalendar 데이터 업데이트 완료"); }
} } catch (error) {
console.error(error);
alert("휴가 저장에 실패했습니다.");
}
}
// /**
onMounted(() => { * 공휴일 데이터 요청 이벤트 변환
const today = new Date(); */
const year = today.getFullYear(); async function fetchHolidays(year, month) {
const month = String(today.getMonth() + 1).padStart(2, "0"); try {
console.log(`📌 초기 로드: ${year}-${month}`); const response = await axios.get(`vacation/${year}/${month}`);
loadCalendarData(year, month); const holidayEvents = response.data.map((holiday) => ({
}); title: holiday.name,
</script> start: holiday.date, // "YYYY-MM-DD"
backgroundColor: "#ff6666",
classNames: ["holiday-event"],
}));
return holidayEvents;
} catch (error) {
console.error("공휴일 정보를 불러오지 못했습니다.", error);
return [];
}
}
/**
* 달력 변경 호출 (FullCalendar의 datesSet 옵션)
*/
function handleMonthChange(viewInfo) {
const currentDate = viewInfo.view.currentStart;
const year = currentDate.getFullYear();
const month = String(currentDate.getMonth() + 1).padStart(2, "0");
loadCalendarData(year, month);
}
/**
* 지정한 월의 데이터를 로드 (휴가, 공휴일 데이터를 병렬 요청)
*/
async function loadCalendarData(year, month) {
fetchedEvents.value = [];
const [vacationEvents, holidayEvents] = await Promise.all([
fetchVacationData(year, month),
fetchHolidays(year, month),
]);
// Set
holidayDates.value = new Set(holidayEvents.map((event) => event.start));
fetchedEvents.value = [...vacationEvents, ...holidayEvents];
updateCalendarEvents();
await nextTick();
fullCalendarRef.value.getApi().refetchEvents();
}
//
onMounted(async () => {
await fetchUserList(); //
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, "0");
await loadCalendarData(year, month);
});
</script>
<style> <style>