fullCalendar 라이브러리는 기본적인 클릭 이벤트나 체인지 이벤트를 제공해준다. 프로젝트 작업 중 컴포넌트 삭제에 대하여 더블 클릭 이벤트를 만들어야 했는데, 해당 라이브러리가 더블 클릭 이벤트를 제공하지 않아 고민하던 중 커스텀 이벤트를 생성해보았다.
방법은 우선 먼저 eventContent 를 사용한다. eventContent는 해당 달력 내에 내가 만든 컴포넌트를 렌더링하게 해준다. 해당 컴포넌트에서 더블 클릭 이벤트를 넣으면 FullCalendar 이벤트를 실행하는 것이 가능하다.
프로젝트의 메인 기능인 캘린더 페이지를 생성하였다. fullCalendar API
를 사용하였고 기존의 할일 등록, 할일 날짜 수정, 할일 기간 연장 이벤트를 우리 프로젝트에 맞게 매핑하였고, 수정 및 삭제 이벤트를 커스텀하여 캘린더 기능을 고도화 하였다. 캘린더 페이지는 다음과 같은 기능을 수행한다. 해당 프로젝트가 솔루션이기 때문에 유저에게 서비스를 좀 더 손쉽게 사용하기 위한 UI 도 제작하였다.
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import {
useDeleteGantt,
useGetGanttChart,
useGetTeamForProject,
usePostCreateGantt,
useUpdateGantt,
} from 'hooks/project';
import { useGetIssuesNotDone } from 'hooks/issue';
import { StyledCalendar, StyledUserImages } from './style';
import FullCalendar from '@fullcalendar/react';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import Notification from 'components/atoms/Notification';
import Circle from 'components/atoms/Circle';
import CalendarGantt from 'components/molecules/CalendarGantt';
import { relative } from 'path';
interface issueType {
ganttChartId: number;
title: string;
start: string;
end: string;
color: string;
issueCode: string;
issueSummary: string;
userId: string;
}
/**
* @description
* Calendar API를 렌더링하는 컴포넌트
* SideBar 지라 이슈들을 가져와 매핑하는 역할을 한다.
*
* @author bell
*/
const index = () => {
const location = useLocation();
const projectId = +location.pathname.split('/')[2];
// react-query
const getGanttChart = useGetGanttChart(1, projectId);
const postCreateGantt = usePostCreateGantt();
const updateGantt = useUpdateGantt();
const deleteGantt = useDeleteGantt();
const getTeamForProject = useGetTeamForProject(projectId);
const getIssuesNotDone = useGetIssuesNotDone(projectId);
const matchColorHandler = (userId: string) => {
if (getTeamForProject.data) {
const idx = getTeamForProject.data.findIndex(item => item.userId === +userId);
if (idx > -1) {
return getTeamForProject.data[idx].userColor;
}
}
return '#FFFFFF';
};
const renderingDBIssuesHandler = () => {
const arr = new Array<issueType>();
if (getGanttChart.data) {
for (const item of getGanttChart.data) {
arr.push({
title: item.issueSummary,
start: item.startTime.split('T')[0],
end: item.endTime.split('T')[0],
color: matchColorHandler(item.userId),
issueSummary: item.issueSummary,
issueCode: item.issueCode,
userId: item.userId,
ganttChartId: item.id,
});
}
}
return arr;
};
useEffect(() => {
if (postCreateGantt.isSuccess) {
getGanttChart.refetch();
}
if (updateGantt.isSuccess) {
getGanttChart.refetch();
}
if (deleteGantt.isSuccess) {
getGanttChart.refetch();
getIssuesNotDone.refetch();
}
}, [postCreateGantt.isSuccess, updateGantt.isSuccess, deleteGantt.isSuccess]);
return (
<StyledCalendar>
{postCreateGantt.isSuccess && (
<Notification
check={true}
message={'이슈가 해당 날짜에 성공적으로 저장되었습니다'}
width={'300px'}
></Notification>
)}
{updateGantt.isSuccess && (
<Notification
check={true}
message={'해당 이슈의 날짜가 수정되었습니다'}
width={'300px'}
></Notification>
)}
{deleteGantt.isSuccess && (
<Notification
check={true}
message={'해당 이슈의 날짜가 삭제되었습니다'}
width={'300px'}
></Notification>
)}
<FullCalendar
plugins={[dayGridPlugin, interactionPlugin]}
timeZone={'UTC'}
initialView={'dayGridMonth'}
selectable={true}
droppable={true}
editable={true}
events={renderingDBIssuesHandler()}
eventResize={({ event, el }) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const props = el.fcSeg.eventRange.def.extendedProps;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const startDateFormat = new Date(event.start).toISOString() as string;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const endDateFormat = new Date(event.end).toISOString() as string;
updateGantt.mutate({
id: props.ganttChartId,
issueCode: props.issueCode,
issueSummary: props.issueSummary,
userId: props.userId,
startTime: startDateFormat,
endTime: endDateFormat,
});
}}
eventReceive={({ event, draggedEl }) => {
const startDateFormat = new Date(new Date(event.startStr).getTime()).toISOString();
const endDateFormat = startDateFormat.split('T')[0] + 'T23:59:59.000Z';
postCreateGantt.mutate({
issueCode: draggedEl.dataset.issue_code as string,
issueSummary: draggedEl.dataset.issue_summary as string,
projectId: Number(draggedEl.dataset.project_id as string),
userId: Number(draggedEl.dataset.user_id),
startTime: startDateFormat,
endTime: endDateFormat,
});
event.remove();
}}
eventDrop={({ event, el }) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const props = el.fcSeg.eventRange.def.extendedProps;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const startDateFormat = new Date(event.start).toISOString() as string;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const endDateFormat = new Date(event.end).toISOString() as string;
console.log(startDateFormat, endDateFormat);
updateGantt.mutate({
id: props.ganttChartId,
issueCode: props.issueCode,
issueSummary: props.issueSummary,
userId: props.userId,
startTime: startDateFormat,
endTime: endDateFormat,
});
}}
eventContent={({ event }) => {
return (
<CalendarGantt
issueCode={event._def.extendedProps.issueCode}
ganttChartId={event._def.extendedProps.ganttChartId}
deleteGantt={deleteGantt}
getGanttChart={getGanttChart}
projectId={projectId}
></CalendarGantt>
);
}}
eventMouseEnter={e => {
e.el.style.transform = 'scale(1.05)';
e.el.style.transition = 'transform 0.1s linear';
}}
eventMouseLeave={e => (e.el.style.transform = 'scale(1)')}
/>
<StyledUserImages>
{getTeamForProject.data &&
getTeamForProject.data.map(item => (
<Circle height="40px" isImage={true} url={item.userImage}>
<div
style={{
width: '40px',
height: '6px',
backgroundColor: `${item.userColor}`,
position: 'relative',
top: '30px',
borderRadius: '10px',
}}
></div>
</Circle>
))}
</StyledUserImages>
</StyledCalendar>
);
};
export default index;