Merge branch 'main' into project-list

This commit is contained in:
yoon 2025-02-13 14:58:51 +09:00
commit 1e1834213b
14 changed files with 586 additions and 448 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 { .half-day-buttons {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -95,8 +94,6 @@
text-align: left !important; text-align: left !important;
} }
/* userList */
.grayscaleImg { .grayscaleImg {
filter: grayscale(100%); filter: grayscale(100%);
} }

View File

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

View File

@ -29,7 +29,7 @@ const commonApi = (options = {}) => {
onMounted(async () => { onMounted(async () => {
// 요청할 데이터가 옵션으로 전달 -> 그에 맞게 호출 // 요청할 데이터가 옵션으로 전달 -> 그에 맞게 호출
// color 옵션에 type 정보 포함 // color 옵션에 type 포함
if (options.loadColor) { if (options.loadColor) {
await CommonCode("user", "color", colorList, options.colorType); await CommonCode("user", "color", colorList, options.colorType);
} }

View File

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

View File

@ -62,17 +62,19 @@ watch(() => props.dislikeCount, (newVal) => {
}); });
const handleLike = () => { const handleLike = () => {
console.log('좋아요',likeCount.value) const isLike = !likeClicked.value;
// emit('updateReaction', { type: 'like', boardId: props.boardId, commentId: props.commentId }); const isDislike = false;
// likeClicked.value = true; emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike: true, isDislike: false }); likeClicked.value = isLike;
dislikeClicked.value = false;
}; };
const handleDislike = () => { const handleDislike = () => {
console.log('싫어요') const isDislike = !dislikeClicked.value;
// emit('updateReaction', { type: 'dislike', boardId: props.boardId, commentId: props.commentId }); const isLike = false;
// dislikeClicked.value = true; emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike, isDislike });
emit('updateReaction', { boardId: props.boardId, commentId: props.commentId, isLike: false, isDislike: true }); dislikeClicked.value = isDislike;
likeClicked.value = false;
}; };
</script> </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,13 +1,6 @@
<template> <template>
<div class="col-xl-12"> <div class="col-xl-12">
<UserFormInput <UserFormInput title="아이디" name="id" :is-alert="idAlert" :useInputGroup="true" @update:data="handleIdChange" :value="id" />
title="아이디"
name="id"
:is-alert="idAlert"
:useInputGroup="true"
@update:data="handleIdChange"
:value="id"
/>
<UserFormInput <UserFormInput
title="비밀번호" title="비밀번호"
@ -78,7 +71,6 @@ const handleSubmit = async () => {
userStore.userInfo(); userStore.userInfo();
router.push('/'); router.push('/');
} }
}) });
}; };
</script> </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: { 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,40 +1,23 @@
<template> <template>
<div class="vacation-management"> <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="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">
<ProfileList />
<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"
/> />
<HalfDayButtons
@toggleHalfDay="toggleHalfDay"
@addVacationRequests="addVacationRequests"
/>
</div> </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>
<br />
</div> </div>
</div> </div>
</div> </div>
@ -48,9 +31,36 @@
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";
const userStore = useUserStore();
const userList = ref([]);
const userColors = ref({});
const fetchUserList = async () => {
try {
await userStore.fetchUserList();
userList.value = userStore.userList;
if (!userList.value.length) {
console.warn("📌 사용자 목록이 비어 있음!");
return;
}
userColors.value = {};
userList.value.forEach((user) => {
userColors.value[user.MEMBERSEQ] = user.usercolor || "#FFFFFF";
});
} catch (error) {
console.error("📌 사용자 목록 불러오기 오류:", error);
}
};
// FullCalendar // FullCalendar
const fullCalendarRef = ref(null); const fullCalendarRef = ref(null);
@ -58,7 +68,6 @@
const fetchedEvents = ref([]); // API (, ) const fetchedEvents = ref([]); // API (, )
const selectedDates = ref(new Map()); // const selectedDates = ref(new Map()); //
const halfDayType = ref(null); const halfDayType = ref(null);
const employeeId = ref(1);
// (YYYY-MM-DD ) ( ) // (YYYY-MM-DD ) ( )
const holidayDates = ref(new Set()); const holidayDates = ref(new Set());
@ -153,16 +162,14 @@
*/ */
async function fetchVacationData(year, month) { async function fetchVacationData(year, month) {
try { try {
console.log(`📌 휴가 데이터 요청: ${year}-${month}`);
const response = await axios.get(`vacation/list/${year}/${month}`); const response = await axios.get(`vacation/list/${year}/${month}`);
if (response.status == 200) { if (response.status == 200) {
const vacationList = response.data; const vacationList = response.data;
console.log("📌 백엔드 응답 데이터:", vacationList);
const events = vacationList const events = vacationList
.map((vac) => { .map((vac) => {
let dateStr = vac.LOCVACUDT.split("T")[0]; let dateStr = vac.LOCVACUDT.split("T")[0];
let className = "fc-daygrid-event"; let className = "fc-daygrid-event";
let backgroundColor = getColorByEmployeeId(vac.MEMBERSEQ); let backgroundColor = userColors.value[vac.MEMBERSEQ] || "#FFFFFF";
let title = "연차"; let title = "연차";
if (vac.LOCVACTYP === "D") { if (vac.LOCVACTYP === "D") {
title = "오전반차"; title = "오전반차";
@ -193,14 +200,6 @@
} }
} }
/**
* 사원 ID별 색상 반환
*/
function getColorByEmployeeId(employeeId) {
const colors = ["#ade3ff", "#ffade3", "#ade3ad", "#ffadad"];
return colors[employeeId % colors.length];
}
/** /**
* 휴가 요청 추가 (선택된 날짜를 백엔드로 전송) * 휴가 요청 추가 (선택된 날짜를 백엔드로 전송)
*/ */
@ -212,7 +211,6 @@
const vacationRequests = Array.from(selectedDates.value).map(([date, type]) => ({ const vacationRequests = Array.from(selectedDates.value).map(([date, type]) => ({
date, date,
type, type,
employeeId: employeeId.value,
})); }));
try { try {
const response = await axios.post("vacation", vacationRequests); const response = await axios.post("vacation", vacationRequests);
@ -239,9 +237,7 @@
*/ */
async function fetchHolidays(year, month) { async function fetchHolidays(year, month) {
try { try {
console.log(`📌 공휴일 요청: ${year}-${month}`);
const response = await axios.get(`vacation/${year}/${month}`); const response = await axios.get(`vacation/${year}/${month}`);
console.log("📌 공휴일 API 응답:", response.data);
const holidayEvents = response.data.map((holiday) => ({ const holidayEvents = response.data.map((holiday) => ({
title: holiday.name, title: holiday.name,
start: holiday.date, // "YYYY-MM-DD" start: holiday.date, // "YYYY-MM-DD"
@ -262,7 +258,6 @@
const currentDate = viewInfo.view.currentStart; 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);
} }
@ -270,30 +265,26 @@
* 지정한 월의 데이터를 로드 (휴가, 공휴일 데이터를 병렬 요청) * 지정한 월의 데이터를 로드 (휴가, 공휴일 데이터를 병렬 요청)
*/ */
async function loadCalendarData(year, month) { async function loadCalendarData(year, month) {
console.log(`📌 ${year}-${month} 데이터 로드 시작`);
fetchedEvents.value = []; fetchedEvents.value = [];
const [vacationEvents, holidayEvents] = await Promise.all([ const [vacationEvents, holidayEvents] = await Promise.all([
fetchVacationData(year, month), fetchVacationData(year, month),
fetchHolidays(year, month), fetchHolidays(year, month),
]); ]);
console.log("📌 변환된 휴가 이벤트:", vacationEvents);
console.log("📌 변환된 공휴일 이벤트:", holidayEvents);
// Set // Set
holidayDates.value = new Set(holidayEvents.map((event) => event.start)); holidayDates.value = new Set(holidayEvents.map((event) => event.start));
fetchedEvents.value = [...vacationEvents, ...holidayEvents]; fetchedEvents.value = [...vacationEvents, ...holidayEvents];
updateCalendarEvents(); updateCalendarEvents();
await nextTick(); await nextTick();
fullCalendarRef.value.getApi().refetchEvents(); fullCalendarRef.value.getApi().refetchEvents();
console.log("📌 FullCalendar 데이터 업데이트 완료");
} }
// //
onMounted(() => { onMounted(async () => {
await fetchUserList(); //
const today = new Date(); const today = new Date();
const year = today.getFullYear(); const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, "0"); const month = String(today.getMonth() + 1).padStart(2, "0");
console.log(`📌 초기 로드: ${year}-${month}`); await loadCalendarData(year, month);
loadCalendarData(year, month);
}); });
</script> </script>