localhost-front/src/components/main/MainEventCalendar.vue

611 lines
21 KiB
Vue

<template>
<div class="card app-calendar-wrapper">
<div class="row g-0">
<div class="col-3 border-end text-center" id="app-calendar-sidebar">
<div class="card-body">
<img
v-if="user"
:src="`${profileImgUrl}profile/${user.profile}`"
alt="Profile Image"
class="w-px-50 h-px-50 rounded-circle"
@error="$event.target.src = '/img/icons/icon.png'"
/>
<p class="mt-2 fw-bold">
{{ user.name }}
</p>
<CommuterBtn :userId="user.id" :checkedInProject="checkedInProject || {}" ref="workTimeComponentRef" />
<MainEventList
:categoryList="categoryList"
:baseUrl="baseUrl"
:birthdayList="birthdayList"
:vacationList="vacationList"
:birthdayPartyList="birthdayPartyList"
:dinnerList="dinnerList"
:teaTimeList="teaTimeList"
:workShopList="workShopList"
/>
</div>
</div>
<div class="col app-calendar-content">
<div class="card shadow-none border-0">
<div class="card-body">
<full-calendar
ref="fullCalendarRef"
:events="calendarEvents"
:options="calendarOptions"
defaultView="dayGridMonth"
class="flatpickr-calendar-only"
>
</full-calendar>
</div>
</div>
</div>
</div>
</div>
<EventModal
v-if="showModal"
:position="modalPosition"
:selected-date="selectedDate"
:base-url="baseUrl"
:date-events="currentDateEvents"
@select="handleEventSelect"
@delete="handleEventDelete"
@insert="handleEventInsert"
@close="handleCloseModal"
/>
</template>
<script setup>
import { inject, onMounted, reactive, ref, watch } from 'vue';
import { fetchHolidays } from '@c/calendar/holiday';
import { useUserInfoStore } from '@/stores/useUserInfoStore';
import { useProjectStore } from '@/stores/useProjectStore';
import { useToastStore } from '@s/toastStore';
import { useWeatherStore } from '@/stores/useWeatherStore';
import { storeToRefs } from 'pinia';
import FullCalendar from '@fullcalendar/vue3';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import CommuterBtn from '@c/commuters/CommuterBtn.vue';
import MainEventList from '@c/main/MainEventList.vue';
import EventModal from '@c/main/EventModal.vue';
import $api from '@api';
import 'flatpickr/dist/flatpickr.min.css';
import '@/assets/css/app-calendar.css';
const baseUrl = import.meta.env.VITE_DOMAIN;
const profileImgUrl = import.meta.env.VITE_SERVER_IMG_URL;
const user = ref({});
const userStore = useUserInfoStore();
const projectStore = useProjectStore();
const weatherStore = useWeatherStore();
const { dailyWeatherList } = storeToRefs(weatherStore);
const dayjs = inject('dayjs');
const fullCalendarRef = ref(null);
const workTimeComponentRef = ref(null);
const calendarEvents = ref([]);
//const dailyWeatherList = ref([]);
const selectedProject = ref(null);
const checkedInProject = ref(null);
// 이벤트 모달 관련
const showModal = ref(false);
const modalPosition = ref({ x: 0, y: 0 });
const selectedDate = ref('');
// 공통 함수
const $common = inject('common');
const toastStore = useToastStore();
// 롱프레스 관련 변수 추가
const pressTimer = ref(null);
const longPressDelay = 500; // 0.5초
/************* category ***************/
// 이벤트 카테고리 데이터 로딩
const categoryList = ref([]);
const fetchCategoryList = async () => {
const { data } = await $api.get('main/category');
if (data) categoryList.value = [...data.data.filter(categoryInfo => categoryInfo.CMNCODODR != 0)];
};
/************* init ***************/
const monthBirthdayList = ref([]);
const monthVacationList = ref([]);
const monthBirthdayPartyList = ref([]);
const monthDinnerList = ref([]);
const monthTeaTimeList = ref([]);
const monthWorkShopList = ref([]);
const birthdayList = ref([]);
const vacationList = ref([]);
const birthdayPartyList = ref([]);
const dinnerList = ref([]);
const teaTimeList = ref([]);
const workShopList = ref([]);
const currentDateEvents = ref([]);
// 생일자, 휴가자, 이벤트 일정 조회
const fetchEventList = async param => {
const { data } = await $api.get(`main/eventList?${param}`);
const res = data?.data;
// 기존의 공휴일 이벤트는 유지
const holidayEvents = calendarEvents.value.filter(event => event.classNames?.includes('holiday-event'));
calendarEvents.value = [...holidayEvents, ...dailyWeatherList.value];
// 생일자
if (res?.memberBirthdayList?.length) {
monthBirthdayList.value = [...res.memberBirthdayList];
res.memberBirthdayList.forEach(member => {
addEvent($common.dateFormatter(member.MEMBERBTH, 'YMD'), 'birthday', `${member.MEMBERNAM}`);
});
}
// 휴가자
if (res?.memberVacationList?.length) {
monthVacationList.value = [...res.memberVacationList];
res.memberVacationList.forEach(member => {
addEvent($common.dateFormatter(member.LOCVACUDT, 'YMD'), 'vacation', `${member.MEMBERNAM}`);
});
}
// 초기화
monthBirthdayPartyList.value = [];
monthDinnerList.value = [];
monthTeaTimeList.value = [];
monthWorkShopList.value = [];
if (res?.eventList?.length) {
res.eventList.forEach(item => {
switch (item.CMNCODVAL) {
case 300203:
monthBirthdayPartyList.value = [...monthBirthdayPartyList.value, item];
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'birthdayParty', '생일파티');
break;
case 300204:
monthDinnerList.value = [...monthDinnerList.value, item];
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'dinner', '회식');
break;
case 300205:
monthTeaTimeList.value = [...monthTeaTimeList.value, item];
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'teaTime', '티타임');
break;
case 300206:
monthWorkShopList.value = [...monthWorkShopList.value, item];
addEvent($common.dateFormatter(item.LOCEVTTME, 'YMD'), 'workshop', '워크샵');
break;
}
});
}
};
// 달력에 이벤트 데이터 추가
const addEvent = (date, type, title) => {
// 생일의 경우 달력의 현재 년도로 변경하여 처리
if (type === 'birthday') {
const calendarApi = fullCalendarRef.value?.getApi();
if (calendarApi) {
const calendarDate = calendarApi.currentData.currentDate;
const { year } = $common.formatDateTime(new Date(calendarDate));
const birthDate = $common.dateFormatter(date, 'MD');
date = `${year}-${birthDate}`;
}
}
// 같은 날짜와 타입의 이벤트가 이미 있는지 확인
const existingEvent = calendarEvents.value.find(
event => $common.dateFormatter(event.start, 'MD') === $common.dateFormatter(date, 'MD') && event.type == type,
);
// 없는 경우에만 추가
if (!existingEvent) {
calendarEvents.value.push({
start: date,
type: type,
title: title,
classNames: [`${type}-event`],
});
}
};
// 해당일 기준 이벤트 리스트 필터링
const useFilterEventList = (month, day) => {
// 생일자
if (monthBirthdayList.value) {
birthdayList.value = $common.filterTargetByDate(monthBirthdayList.value, 'MEMBERBTH', month, day);
}
// 휴가자
if (monthVacationList.value) {
vacationList.value = $common.filterTargetByDate(monthVacationList.value, 'LOCVACUDT', month, day);
}
// 생일파티
if (monthBirthdayPartyList.value) {
birthdayPartyList.value = $common.filterTargetByDate(monthBirthdayPartyList.value, 'LOCEVTTME', month, day);
}
// 회식
if (monthDinnerList.value) {
dinnerList.value = $common.filterTargetByDate(monthDinnerList.value, 'LOCEVTTME', month, day);
}
// 티타임
if (monthTeaTimeList.value) {
teaTimeList.value = $common.filterTargetByDate(monthTeaTimeList.value, 'LOCEVTTME', month, day);
}
// 워크샵
if (monthWorkShopList.value) {
workShopList.value = $common.filterTargetByDate(monthWorkShopList.value, 'LOCEVTTME', month, day);
}
};
// 캘린더 데이터 가져오기
const fetchData = async () => {
// FullCalendar API 인스턴스 가져오기
const calendarApi = fullCalendarRef.value?.getApi();
if (!calendarApi) return;
const date = calendarApi.currentData.currentDate;
const { year, month } = $common.formatDateTime(new Date(date));
try {
// 현재 표시 중인 월의 공휴일 정보 가져오기
const holidayEvents = await fetchHolidays(year, month);
calendarEvents.value = [...holidayEvents]; // 공휴일 정보로 초기화
// 이벤트 데이터 가져오기
const param = new URLSearchParams();
param.append('year', year);
param.append('month', month);
param.append('day', '1'); // 해당 월의 첫날
await fetchEventList(param);
} catch (error) {
console.error('공휴일 정보 로딩 실패:', error);
}
};
// 캘린더 이동 함수 (이전, 다음, 오늘)
const moveCalendar = async (value = 0) => {
const calendarApi = fullCalendarRef.value?.getApi();
if (value === 1) {
calendarApi.prev(); // 이전 달로 이동
} else if (value === 2) {
calendarApi.next(); // 다음 달로 이동
} else if (value === 3) {
calendarApi.today(); // 오늘 날짜로 이동
}
await fetchData();
};
// 날짜 선택 가능 여부를 확인하는 공통 함수
const isSelectableDate = date => {
const checkDate = dayjs(date);
const isWeekend = checkDate.day() === 0 || checkDate.day() === 6; // 주말체크
// 공휴일 체크
const isHoliday = calendarEvents.value.some(
event =>
event.classNames?.includes('holiday-event') && dayjs(event.start).format('YYYY-MM-DD') === checkDate.format('YYYY-MM-DD'),
);
return !isWeekend && !isHoliday;
};
// 날짜 셀 클래스 추가 함수
const getCellClassNames = arg => {
const cellDate = dayjs(arg.date);
const classes = [];
// 선택 불가능한 날짜(과거, 주말, 공휴일)에 동일한 클래스 추가
if (!isSelectableDate(cellDate)) {
classes.push('fc-day-sat-sun');
} else {
// 선택 가능한 날짜 포인터 클래스 추가
classes.push('clickable');
}
return classes;
};
// 날짜 클릭 이벤트 핸들러
let todayEL = null;
const handleDateClick = info => {
if (isSelectableDate(info.date)) {
if ($common.isToday(info.date)) {
// 오늘 날짜 클릭 시 클래스 제거하고 요소 저장
todayEL = info.dayEl;
todayEL.classList.remove('fc-day-today');
} else if (todayEL) {
// 다른 날짜 클릭 시 저장된 오늘 요소에 클래스 다시 추가
todayEL.classList.add('fc-day-today');
todayEL = null;
}
}
const { month, day } = $common.formatDateTime(new Date(info.dateStr));
useFilterEventList(month, day);
};
// 이벤트 모달 핸들러
const handleMouseDown = (date, jsEvent) => {
if (showModal.value) showModal.value = false;
// 해당 날짜의 이벤트 필터링
const dateEvents = calendarEvents.value.filter(
event => $common.dateFormatter(event.start, 'YMD') === $common.dateFormatter(date, 'YMD'),
);
pressTimer.value = setTimeout(() => {
modalPosition.value = {
x: jsEvent.clientX,
y: jsEvent.clientY,
};
selectedDate.value = date;
currentDateEvents.value = dateEvents;
showModal.value = true;
pressTimer.value = null;
}, longPressDelay);
};
// 이벤트 모달 외부 클릭 시 닫힘
const handleMouseUp = () => {
if (pressTimer.value) {
clearTimeout(pressTimer.value);
pressTimer.value = null;
}
};
// 이벤트 삭제 api
const toggleEvent = async (date, code, title) => {
const { data } = await $api.post('main/toggleEvent', {
date: date,
code: code,
title: title,
});
if (data?.code === 200) toastStore.onToast(data.message);
const { year, month, day } = $common.formatDateTime(new Date(date));
const param = new URLSearchParams();
param.append('year', year);
param.append('month', month);
param.append('day', day);
await fetchEventList(param);
useFilterEventList(month, day);
};
// 이벤트 추가 api
const insertEvent = async (date, code, title, place, time) => {
const dateTime = $common.dateFormatter(`${date} ${time}`);
const { data } = await $api.post('main/inserEvent', {
date: dateTime,
code: code,
title: title,
place: place,
});
if (data?.code === 200) toastStore.onToast(data.message);
const { year, month, day } = $common.formatDateTime(new Date(date));
const param = new URLSearchParams();
param.append('year', year);
param.append('month', month);
param.append('day', day);
await fetchEventList(param);
useFilterEventList(month, day);
};
// 이벤트 선택 핸들러
const handleEventSelect = data => {
toggleEvent(data.date, data.code, data.title);
showModal.value = false;
};
// 이벤트 추가 핸들러
const handleEventInsert = data => {
insertEvent(data.date, data.code, data.title, data.place, data.time);
showModal.value = false;
};
// 이벤트 삭제 핸들러
const handleEventDelete = data => {
toggleEvent(data.date, data.code, data.title);
showModal.value = false;
};
// 이벤트 모달 닫기
const handleCloseModal = () => {
showModal.value = false;
};
// 달력 이벤트 아이콘 표시 함수
const handleEventContent = item => {
if (!item.event) return null;
// 공휴일인 경우 텍스트로 표시
if (item.event.classNames?.includes('holiday-event')) {
return {
html: `<div class="holiday-text" style="color: white;">${item.event.title}</div>`,
};
}
// 현재 이벤트의 타입만 확인
const eventType = item.event.extendedProps.type;
if (!eventType) return null;
let iconCode = '';
switch (eventType) {
case 'birthday':
iconCode = '300201';
break;
case 'vacation':
iconCode = '300202';
break;
case 'birthdayParty':
iconCode = '300203';
break;
case 'dinner':
iconCode = '300204';
break;
case 'teaTime':
iconCode = '300205';
break;
case 'workshop':
iconCode = '300206';
break;
default:
return null;
}
return {
html: `<img src="${baseUrl}img/main-category-img/main-${iconCode}.png" class="calendar-event-icon" style="width: 20px; height: 20px; margin: 2px;" />`,
};
};
// 캘린더 옵션 설정
const calendarOptions = reactive({
plugins: [dayGridPlugin, interactionPlugin],
initialView: 'dayGridMonth',
headerToolbar: {
left: 'today',
center: 'title',
right: 'prev,next',
},
locale: 'kr',
events: calendarEvents,
eventOrder: 'sortIdx',
contentHeight: 'auto',
eventContent: handleEventContent,
selectable: true,
selectAllow: selectInfo => isSelectableDate(selectInfo.start),
dateClick: handleDateClick,
dayCellDidMount: arg => {
const dateCell = arg.el;
// 마우스 홀드시 이벤트 모달
dateCell.addEventListener('mousedown', e => {
const date = $common.dateFormatter(arg.date, 'YMD');
handleMouseDown(date, e);
});
dateCell.addEventListener('mouseup', handleMouseUp);
dateCell.addEventListener('mouseleave', handleMouseUp);
},
dayCellClassNames: getCellClassNames,
unselectAuto: true,
droppable: false,
eventDisplay: 'block',
customButtons: {
prev: {
text: 'PREV',
click: () => moveCalendar(1),
},
today: {
text: 'TODAY',
click: () => moveCalendar(3),
},
next: {
text: 'NEXT',
click: () => moveCalendar(2),
},
},
});
const handleWheelEvent = e => {
handleCloseModal();
};
// 달력 뷰 변경 감지 (월 변경 시 데이터 다시 가져오기)
watch(
() => fullCalendarRef.value?.getApi().currentData.viewTitle,
async () => {
await fetchData();
},
);
onMounted(async () => {
await userStore.userInfo();
user.value = userStore.user;
// 저장된 선택 프로젝트 가져오기
const storedProject = projectStore.getSelectedProject();
if (storedProject) {
selectedProject.value = storedProject.PROJCTSEQ;
checkedInProject.value = storedProject;
}
// 오늘 기준 데이터 호출
const { year, month, day } = $common.getToday();
const param = new URLSearchParams();
param.append('year', year);
param.append('month', month);
param.append('day', day);
if (!weatherStore.dailyWeatherList?.length) {
await weatherStore.getWeatherInfo();
//dailyWeatherList.value = weatherStore.dailyWeatherList;
console.log('dailyWeatherList.value: ', dailyWeatherList.value);
}
// 이벤트 카테고리 호출
await fetchCategoryList();
await fetchEventList(param);
useFilterEventList(month, day);
// 스크롤 감지 이벤트 리스너
window.addEventListener('wheel', handleWheelEvent);
});
</script>
<style scoped>
::v-deep(.fc-h-event) {
background-color: transparent;
}
::v-deep(.event-modal) {
padding: 8px;
border: 1px solid #ddd;
}
::v-deep(.event-icon-select:hover) {
transform: scale(1.1);
transition: transform 0.2s;
}
/* 이벤트 모달 노출 시 텍스트 선택 방지 */
::v-deep(.fc-daygrid-day) {
user-select: none;
}
::v-deep(.fc-daygrid-day-events) {
display: flex;
flex-wrap: wrap;
justify-content: center;
/* align-content: flex-start;
align-items: center;
text-align: center !important; */
}
/* 공휴일만 가로로 넗게 나오게 */
::v-deep(.fc-daygrid-event-harness:has(.holiday-event)) {
width: 100% !important;
}
</style>