This commit is contained in:
yoon 2025-02-13 14:50:54 +09:00
commit 264bb09dd5
13 changed files with 600 additions and 459 deletions

5
.env.dev Normal file
View File

@ -0,0 +1,5 @@
VITE_DOMAIN = http://localhost:5173/
# VITE_LOGIN_URL = http://localhost:10325/ms/
# VITE_FILE_URL = http://localhost:10325/ms/
# VITE_API_URL = http://localhost:10325/api/
VITE_API_URL = http://localhost:10325/test/

View File

@ -29,7 +29,6 @@
/* 휴가*/
.half-day-buttons {
display: flex;
justify-content: center;
@ -93,11 +92,4 @@
top: 0px !important;
left: 5px !important;
text-align: left !important;
}
/* userList */
.grayscaleImg {
filter: grayscale(100%);
}
}

View File

@ -1,12 +1,12 @@
import axios from "axios";
import axios from 'axios';
import { useRoute } from 'vue-router';
import { useToastStore } from '@s/toastStore';
const $api = axios.create({
baseURL: 'http://localhost:10325/api/',
timeout: 300000,
withCredentials : true
})
withCredentials: true,
});
/**
* Default Content-Type : json
@ -14,7 +14,6 @@ const $api = axios.create({
*/
$api.interceptors.request.use(
function (config) {
let contentType = 'application/json';
if (config.isFormData) contentType = 'multipart/form-data';
@ -23,21 +22,21 @@ $api.interceptors.request.use(
config.headers['X-Requested-With'] = 'XMLHttpRequest';
return config;
}, function (error) {
},
function (error) {
// 요청 오류가 있는 작업 수행
return Promise.reject(error);
}
},
);
// 응답 인터셉터 추가하기
$api.interceptors.response.use(
function (response) {
// 2xx 범위의 응답 처리
return response;
},
function (error) {
const toastStore = useToastStore()
const toastStore = useToastStore();
const currentPage = error.config.headers['X-Page-Route'];
// 오류 응답 처리
if (error.response) {
@ -70,7 +69,7 @@ $api.interceptors.response.use(
}
return Promise.reject(error);
}
},
);
export default $api;

View File

@ -25,22 +25,17 @@
<EditButton @click="handleEdit" />
<DeleteButton @click="handleDelete" />
<div class="input-group mt-3" v-if="isPassword && unknown">
<input
type="password"
v-model="password"
class="form-control"
placeholder="비밀번호 입력"
/>
<!-- <input
type="password"
class="form-control"
placeholder="비밀번호 입력"
:is-alert="idAlert"
@update:data="handleIdChange"
:value="id"
/> -->
<button class="btn btn-primary" type="button" @click="handlePasswordSubmit">확인</button>
<div class="mt-3" v-if="isPassword && unknown">
<div class="input-group">
<input
type="password"
class="form-control"
v-model="password"
placeholder="비밀번호 입력"
/>
<button class="btn btn-primary" type="button" @click="handleSubmit">확인</button>
</div>
<span v-if="passwordAlert" class="invalid-feedback d-block text-start">{{ passwordAlert }}</span>
</div>
</div>
</template>
@ -71,6 +66,10 @@ import BoardRecommendBtn from '../button/BoardRecommendBtn.vue';
// Vue Router
const router = useRouter();
const isPassword = ref(false);
const password = ref('');
const passwordAlert = ref(false);
const lastClickedButton = ref('');
// Props
const props = defineProps({
@ -112,22 +111,14 @@ const props = defineProps({
}
});
const isPassword = ref(false);
const password = ref('');
const lastClickedButton = ref('');
const boardId = 100; //!
const emit = defineEmits(['togglePasswordInput']);
//
const handleEdit = () => {
// router.push({ name: 'BoardEdit', params: { id: boardId } });
if (props.unknown) {
togglePassword('edit');
} else {
router.push({ name: 'BoardEdit', params: { id: 100 } }); //
router.push({ name: 'BoardEdit', params: { id: props.boardId } });
}
};
@ -136,7 +127,7 @@ const handleDelete = () => {
if (props.unknown) {
togglePassword('delete');
} else {
deletePost(); //
deletePost();
}
};
@ -151,12 +142,11 @@ const togglePassword = (button) => {
};
//
const handlePasswordSubmit = async () => {
isPassword.value = false;
lastClickedButton.value = null;
console.log('비밀번호:', password.value);
console.log(props.boardId)
const handleSubmit = async () => {
if (!password.value) {
passwordAlert.value = '비밀번호를 입력해주세요.';
return;
}
try {
const requestData = {
@ -164,47 +154,61 @@ const handlePasswordSubmit = async () => {
LOCBRDSEQ: 288
}
console.log(requestData)
const postResponse = await axios.post(`board/${props.boardId}/password`, requestData);
console.log('post결과:', postResponse.data)
if (postResponse.data.code === 200) {
if (postResponse.data.data === true) {
isPassword.value = false;
// if (response.data.code === 200 && response.data.data === true) {
// console.log(""); //
// } else {
// console.log(" .");
// }
if (lastClickedButton.value === 'edit') {
router.push({ name: 'BoardEdit', params: { id: props.boardId } });
} else if (lastClickedButton.value === 'delete') {
await deletePost();
}
lastClickedButton.value = null;
} else {
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
}
} else {
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
}
} catch (error) {
console.error('비밀번호 확인 중 오류 발생:', error);
// 401
if (error.response && error.response.status === 401) {
passwordAlert.value = '비밀번호가 일치하지 않습니다.';
} else if (error.response) {
alert(`오류 발생: ${error.response.data.message || '서버 오류'}`);
} else {
alert('네트워크 오류가 발생했습니다. 다시 시도해주세요.');
}
}
};
const deletePost = async () => {
if (confirm('정말 삭제하시겠습니까?')) {
try {
await axios.delete(`board/100`);
alert('게시물이 삭제되었습니다.');
router.push({ name: 'BoardList' });
const response = await axios.delete(`board/${props.boardId}`, {
data: { LOCBRDSEQ: props.boardId }
});
if (response.data.code === 200) {
alert('게시물이 삭제되었습니다.');
router.push({ name: 'BoardList' });
} else {
alert('삭제 실패: ' + response.data.message);
}
} catch (error) {
alert('삭제 중 오류 발생');
if (error.response) {
alert(`삭제 실패: ${error.response.data.message || '서버 오류'}`);
} else {
alert('네트워크 오류가 발생했습니다. 다시 시도해주세요.');
}
}
}
};
// const fetchBoardDetails = async () => {
// try {
// const response = await axios.get(`board/${props.boardId}`);
// console.log(response.data);
// } catch (error) {
// console.error(' :', error);
// }
// };
// onMounted(() => {
// // fetchBoardDetails();
// });
</script>
<style scoped>

View File

@ -62,17 +62,19 @@ watch(() => props.dislikeCount, (newVal) => {
});
const handleLike = () => {
console.log('좋아요',likeCount.value)
// emit('updateReaction', { type: 'like', boardId: props.boardId, commentId: props.commentId });
// likeClicked.value = true;
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike: true, isDislike: false });
const isLike = !likeClicked.value;
const isDislike = false;
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
likeClicked.value = isLike;
dislikeClicked.value = false;
};
const handleDislike = () => {
console.log('싫어요')
// emit('updateReaction', { type: 'dislike', boardId: props.boardId, commentId: props.commentId });
// dislikeClicked.value = true;
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike: false, isDislike: true });
const isDislike = !dislikeClicked.value;
const isLike = false;
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
dislikeClicked.value = isDislike;
likeClicked.value = false;
};
</script>

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,92 @@
<template>
<div class="input-group">
<input
:id="name"
class="form-control"
:type="type"
v-model="inputValue"
:maxLength="maxlength"
:placeholder="isLabel ? '' : title"
/>
<button class="btn btn-primary" type="button" @click="handleSubmit">확인</button>
</div>
<div class="invalid-feedback" :class="isAlert ? 'display-block' : ''">
{{ title }} 확인해주세요.
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
// Props
const props = defineProps({
title: {
type: String,
default: '라벨',
},
name: {
type: String,
default: 'nameplz',
},
isEssential: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'text',
},
modelValue: {
type: String,
default: '',
},
maxlength: {
type: Number,
default: 30,
},
isAlert: {
type: Boolean,
default: false,
},
isLabel: {
type: Boolean,
default: true,
},
});
// Emits
const emits = defineEmits(['update:modelValue', 'submit']);
// `inputValue`
const inputValue = ref(props.modelValue);
//
watch(inputValue, (newValue) => {
emits('update:modelValue', newValue);
});
//
watch(() => props.modelValue, (newValue) => {
if (inputValue.value !== newValue) {
inputValue.value = newValue;
}
});
// submit
const handleSubmit = () => {
emits('submit', inputValue.value);
};
</script>
<style scoped>
.invalid-feedback {
display: none;
color: red;
font-size: 0.875rem;
margin-top: 4px;
}
.display-block {
display: block;
}
</style>

View File

@ -1,14 +1,7 @@
<template>
<form @submit.prevent="handleSubmit">
<div class="col-xl-12">
<UserFormInput
title="아이디"
name="id"
:is-alert="idAlert"
:useInputGroup="true"
@update:data="handleIdChange"
:value="id"
/>
<UserFormInput title="아이디" name="id" :is-alert="idAlert" :useInputGroup="true" @update:data="handleIdChange" :value="id" />
<UserFormInput
title="비밀번호"
@ -63,17 +56,24 @@
};
const handleSubmit = async () => {
$api.post('user/login', {
loginId: id.value,
password: password.value,
remember: remember.value,
}, { headers: { 'X-Page-Route': route.path } })
.then(res => {
$api.post(
'user/login',
{
loginId: id.value,
password: password.value,
remember: remember.value,
},
{ headers: { 'X-Page-Route': route.path } },
).then(res => {
if (res.status === 200) {
// TODO:
const sessionCookie = res.data.data;
document.cookie = `JSESSIONID=${sessionCookie};path=/;expires=-1;`;
document.cookie = `JSESSIONID=${sessionCookie};path=/`;
userStore.userInfo();
router.push('/');
}
})
});
};
</script>

View File

@ -0,0 +1,107 @@
<template>
<div class="card-body d-flex justify-content-center">
<ul class="list-unstyled d-flex align-items-center gap-7 mb-0 mt-2">
<li
v-for="(user, index) in sortedUserList"
:key="index"
:class="{ disabled: user.disabled }"
@click="toggleDisable(index)"
data-bs-placement="top"
:aria-label="user.MEMBERSEQ"
>
<img
class="rounded-circle user-avatar"
:src="getUserProfileImage(user.MEMBERPRF)"
alt="user"
:style="getDynamicStyle(user)"
@error="setDefaultImage"
@load="showImage"
/>
</li>
</ul>
</div>
</template>
<script setup>
import { onMounted, ref, computed, nextTick } from "vue";
import { useUserStore } from "@s/useUserStore"; //
import { useUserStore as useUserListStore } from "@s/userList"; //
import $api from "@api";
const userStore = useUserStore();
const userListStore = useUserListStore();
const userList = ref([]);
const userListContainer = ref(null);
const baseUrl = $api.defaults.baseURL.replace(/api\/$/, "");
const defaultProfile = "/img/icons/icon.png";
const employeeId = ref(null); // ID
onMounted(async () => {
await userStore.userInfo(); //
await userListStore.fetchUserList(); //
userList.value = userListStore.userList;
// ID
if (userStore.user) {
employeeId.value = userStore.user.id;
}
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; //
//
const myProfile = userList.value.find(user => user.MEMBERSEQ === employeeId.value);
const otherUsers = userList.value.filter(user => user.MEMBERSEQ !== employeeId.value);
return myProfile ? [myProfile, ...otherUsers] : userList.value;
});
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 profileSize = computed(() => {
const totalUsers = userList.value.length;
if (totalUsers <= 7) return "120px"; // 7
if (totalUsers <= 10) return "110px"; // ~10
if (totalUsers <= 20) return "80px"; // ~20
return "60px"; // 20
});
//
const getDynamicStyle = (user) => {
return {
width: profileSize.value,
height: profileSize.value,
borderWidth: "3px",
borderColor: user.usercolor || "#ccc",
};
};
</script>
<style scoped>
</style>

View File

@ -16,7 +16,6 @@ export const useUserStore = defineStore("userStore", {
actions: {
async fetchUserList() {
const response = await axios.get('user/allUserList');
console.log('response',response)
this.userList = response.data.data.allUserList;
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,292 @@
<template>
<div class="vacation-management">
<div class="container flex-grow-1">
<div class="container-xxl flex-grow-1 container-p-y">
<div class="card app-calendar-wrapper">
<div class="row g-0">
<div class="row g-0">
<div class="col app-calendar-content">
<div class="card shadow-none border-0">
<div class="card shadow-none border-0">
<ProfileList />
<div class="card-body">
<full-calendar
<full-calendar
ref="fullCalendarRef"
:options="calendarOptions"
class="flatpickr-calendar-only"
/>
/>
<HalfDayButtons
@toggleHalfDay="toggleHalfDay"
@addVacationRequests="addVacationRequests"
/>
</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>
<br />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</template>
<script setup>
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import "flatpickr/dist/flatpickr.min.css";
import "@/assets/css/app-calendar.css";
import { reactive, ref, onMounted, nextTick } from "vue";
import axios from "@api";
import "bootstrap-icons/font/bootstrap-icons.css";
<script setup>
import FullCalendar from "@fullcalendar/vue3";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import "flatpickr/dist/flatpickr.min.css";
import "@/assets/css/app-calendar.css";
import { reactive, ref, onMounted, nextTick, watchEffect } from "vue";
import axios from "@api";
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 fullCalendarRef = ref(null);
const calendarEvents = ref([]); // FullCalendar (API + )
const fetchedEvents = ref([]); // API (, )
const selectedDates = ref(new Map()); //
const halfDayType = ref(null);
const employeeId = ref(1);
const userStore = useUserStore();
const userList = ref([]);
const userColors = ref({});
// (YYYY-MM-DD ) ( )
const holidayDates = ref(new Set());
// FullCalendar (events calendarEvents )
const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin],
initialView: "dayGridMonth",
headerToolbar: {
left: "today",
center: "title",
right: "prev,next",
},
locale: "ko",
selectable: false,
dateClick: handleDateClick,
datesSet: handleMonthChange,
events: calendarEvents,
});
const fetchUserList = async () => {
try {
await userStore.fetchUserList();
userList.value = userStore.userList;
/**
* 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 {
className = "selected-full"; //
title = "연차 (선택)";
}
return {
title,
start: date,
backgroundColor: "rgba(0, 128, 0, 0.3)",
display: "background",
classNames: [className],
};
if (!userList.value.length) {
console.warn("📌 사용자 목록이 비어 있음!");
return;
}
userColors.value = {};
userList.value.forEach((user) => {
userColors.value[user.MEMBERSEQ] = user.usercolor || "#FFFFFF";
});
calendarEvents.value = [...fetchedEvents.value, ...selectedEvents];
} catch (error) {
console.error("📌 사용자 목록 불러오기 오류:", error);
}
};
/**
* 날짜 클릭 이벤트
* - 주말(, ) 공휴일은 클릭되지 않음
* - 클릭 해당 날짜를 selectedDates에 추가 또는 제거한 updateCalendarEvents() 호출
*/
function handleDateClick(info) {
const clickedDateStr = info.dateStr;
const clickedDate = info.date;
// FullCalendar
const fullCalendarRef = ref(null);
const calendarEvents = ref([]); // FullCalendar (API + )
const fetchedEvents = ref([]); // API (, )
const selectedDates = ref(new Map()); //
const halfDayType = ref(null);
// (:6, :0)
if (clickedDate.getDay() === 0 || clickedDate.getDay() === 6) {
return;
}
//
if (holidayDates.value.has(clickedDateStr)) {
return;
}
if (!selectedDates.value.has(clickedDateStr)) {
const type = halfDayType.value
? halfDayType.value === "AM"
? "D"
: "N"
: "F";
selectedDates.value.set(clickedDateStr, type);
// (YYYY-MM-DD ) ( )
const holidayDates = ref(new Set());
// FullCalendar (events calendarEvents )
const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin],
initialView: "dayGridMonth",
headerToolbar: {
left: "today",
center: "title",
right: "prev,next",
},
locale: "ko",
selectable: false,
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 {
selectedDates.value.delete(clickedDateStr);
className = "selected-full"; //
title = "연차 (선택)";
}
halfDayType.value = null;
updateCalendarEvents();
}
return {
title,
start: date,
backgroundColor: "rgba(0, 128, 0, 0.3)",
display: "background",
classNames: [className],
};
});
calendarEvents.value = [...fetchedEvents.value, ...selectedEvents];
}
/**
* 오전/오후 반차 버튼 토글
*/
function toggleHalfDay(type) {
halfDayType.value = halfDayType.value === type ? null : type;
}
/**
* 날짜 클릭 이벤트
* - 주말(, ) 공휴일은 클릭되지 않음
* - 클릭 해당 날짜를 selectedDates에 추가 또는 제거한 updateCalendarEvents() 호출
*/
function handleDateClick(info) {
const clickedDateStr = info.dateStr;
const clickedDate = info.date;
/**
* 백엔드에서 휴가 데이터를 가져와 이벤트로 변환
*/
async function fetchVacationData(year, month) {
try {
console.log(`📌 휴가 데이터 요청: ${year}-${month}`);
const response = await axios.get(`vacation/list/${year}/${month}`);
if (response.status == 200) {
const vacationList = response.data;
console.log("📌 백엔드 응답 데이터:", vacationList);
const events = vacationList
.map((vac) => {
let dateStr = vac.LOCVACUDT.split("T")[0];
let className = "fc-daygrid-event";
let backgroundColor = getColorByEmployeeId(vac.MEMBERSEQ);
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 [];
// (:6, :0)
if (clickedDate.getDay() === 0 || clickedDate.getDay() === 6) {
return;
}
//
if (holidayDates.value.has(clickedDateStr)) {
return;
}
if (!selectedDates.value.has(clickedDateStr)) {
const type = halfDayType.value
? halfDayType.value === "AM"
? "D"
: "N"
: "F";
selectedDates.value.set(clickedDateStr, type);
} else {
selectedDates.value.delete(clickedDateStr);
}
halfDayType.value = null;
updateCalendarEvents();
}
/**
* 오전/오후 반차 버튼 토글
*/
function toggleHalfDay(type) {
halfDayType.value = halfDayType.value === type ? null : type;
}
/**
* 백엔드에서 휴가 데이터를 가져와 이벤트로 변환
*/
async function fetchVacationData(year, month) {
try {
const response = await axios.get(`vacation/list/${year}/${month}`);
if (response.status == 200) {
const vacationList = response.data;
const events = vacationList
.map((vac) => {
let dateStr = vac.LOCVACUDT.split("T")[0];
let className = "fc-daygrid-event";
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) {
const colors = ["#ade3ff", "#ffade3", "#ade3ad", "#ffadad"];
return colors[employeeId % colors.length];
}
/**
* 휴가 요청 추가 (선택된 날짜를 백엔드로 전송)
*/
async function addVacationRequests() {
if (selectedDates.value.size === 0) {
alert("휴가를 선택해주세요.");
return;
}
const vacationRequests = Array.from(selectedDates.value).map(([date, type]) => ({
date,
type,
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;
/**
* 휴가 요청 추가 (선택된 날짜를 백엔드로 전송)
*/
async function addVacationRequests() {
if (selectedDates.value.size === 0) {
alert("휴가를 선택해주세요.");
return;
}
const vacationRequests = Array.from(selectedDates.value).map(([date, type]) => ({
date,
type,
}));
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");
console.log(`📌 월 변경 감지: ${year}-${month}`);
loadCalendarData(year, month);
}
/**
* 지정한 월의 데이터를 로드 (휴가, 공휴일 데이터를 병렬 요청)
*/
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];
selectedDates.value.clear();
updateCalendarEvents();
await nextTick();
fullCalendarRef.value.getApi().refetchEvents();
console.log("📌 FullCalendar 데이터 업데이트 완료");
}
} else {
alert("휴가 저장 중 오류가 발생했습니다.");
}
} catch (error) {
console.error(error);
alert("휴가 저장에 실패했습니다.");
}
}
//
onMounted(() => {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, "0");
console.log(`📌 초기 로드: ${year}-${month}`);
loadCalendarData(year, month);
});
</script>
/**
* 공휴일 데이터 요청 이벤트 변환
*/
async function fetchHolidays(year, month) {
try {
const response = await axios.get(`vacation/${year}/${month}`);
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 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>