본문 바로가기
개발/Spring

[SpringBoot] FullCalendar을 이용한 캘린더 구현하기

by 코딩하는 흰둥이 2024. 7. 23.
cdn
<!-- Tyhmeleaf 미적용 -->
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js"></script>

<!-- Tyhmeleaf 적용 -->
<script th:src="@{https://cdn.jsdelivr.net/npm/fullcalendar@6.1.15/index.global.min.js}"></script>

 

 

 

vo
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CalendarVo {
        private Long calendarNo;
        private String title;
        private String start1;
        private String end;
        private boolean allDay;
}

 

 

controller
import com.example.practice.service.calendar.CalendarService;
import com.example.practice.vo.calendar.CalendarVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;

@RestController
public class CalendarController {

    @Autowired
    private CalendarService calendarService;

    /**
     * 캘린더 일정 조회하기
     * @return
     * @throws Exception
     */
    @RequestMapping("/calendarList")
    public List<CalendarVo> calendarList() throws Exception{
        List<CalendarVo> vo = calendarService.calendarList();

        return vo;
    }

    /**
     * 캘린더 일정 추가하기
     * @param map
     * @return
     * @throws Exception
     */
    @PostMapping("/calendarSave")
    public CalendarVo calendarSave(@RequestBody Map<String, Object> map) throws Exception {

        CalendarVo vo = new CalendarVo();
        vo.setTitle((String) map.get("title"));

        // UTC 시간을 LocalDateTime으로 변환
        DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME;
        ZonedDateTime startUTC = ZonedDateTime.parse((String) map.get("start"), formatter).withZoneSameInstant(ZoneId.of("Asia/Seoul"));
        ZonedDateTime endUTC = map.get("end") != null ? ZonedDateTime.parse((String) map.get("end"), formatter).withZoneSameInstant(ZoneId.of("Asia/Seoul")) : null;

        // 한국 시간으로 변환하여 저장
        vo.setStart1(startUTC.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        vo.setEnd(endUTC != null ? endUTC.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) : null);
        vo.setAllDay((Boolean) map.get("allDay"));

        // 저장한 일정의 key 값을 포함한 데이터를 다시 반환
        calendarService.calendarSave(vo);

        return vo;
    }

    /**
     * 캘린더 일정 삭제하기
     * @param no
     * @return
     * @throws Exception
     */
    @DeleteMapping("/calendarDelete")
    public String calendarDelete(@RequestParam String no) throws Exception{
        try{
            calendarService.calendarDelete(no);
            return "success";
        }catch (Exception e){
            e.printStackTrace();
            return "fail";
        }
    }


    /**
     * 캘린더 일정 수정하기
     * @param no
     * @param map
     * @return
     */
    @PutMapping("/eventUpdate/{no}")
    public String eventUpdate(@PathVariable String no, @RequestBody Map<String, Object> map){

        CalendarVo vo = new CalendarVo();
        vo.setCalendarNo(Long.valueOf(no));
        vo.setTitle((String) map.get("title"));
        vo.setStart1(map.get("start1").toString().substring(0, 19));
        if(map.get("end") != null){
            vo.setEnd(map.get("end").toString().substring(0, 19));
        }
        vo.setAllDay((Boolean) map.get("allDay"));

        try {
            calendarService.eventUpdate(vo);
            return "success";
        } catch (Exception e) {
            e.printStackTrace();
            return "fail";
        }
    }

}

 

 

 

 

 

service
import com.example.practice.vo.calendar.CalendarVo;
import java.util.List;

public interface CalendarService {

    /**
     * 캘린더 일정 조회하기
     * @return
     * @throws Exception
     */
    List<CalendarVo> calendarList() throws Exception;

    /**
     * 캘린더 일정 저장하기
     * @param vo
     * @throws Exception
     */
    void calendarSave(CalendarVo vo) throws Exception;

    /**
     * 캘린더 일정 삭제하기
     * @param no
     * @throws Exception
     */
    void calendarDelete(String no) throws Exception;

    /**
     * 캘린더 일정 수정하기
     * @param vo
     * @throws Exception
     */
    void eventUpdate(CalendarVo vo) throws Exception;
}

 

 

mapper.class
import com.example.practice.vo.calendar.CalendarVo;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface CalendarMapper {

    /**
     * 캘린더 일정 조회하기
     * @return
     * @throws Exception
     */
    List<CalendarVo> calendarList() throws Exception;

    /**
     * 캘린더 일정 저장하기
     * @param vo
     * @throws Exception
     */
    void calendarSave(CalendarVo vo) throws Exception;

    /**
     * 캘린더 일정 삭제하기
     * @param no
     * @throws Exception
     */
    void calendarDelete(String no) throws Exception;

    /**
     * 캘린더 일정 수정하기
     * @param vo
     * @throws Exception
     */
    void eventUpdate(CalendarVo vo) throws Exception;
}

 

 

serviceImpl
import com.example.practice.mapper.calendar.CalendarMapper;
import com.example.practice.service.calendar.CalendarService;
import com.example.practice.vo.calendar.CalendarVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class CalendarServiceImpl implements CalendarService {

    @Autowired
    private CalendarMapper calendarMapper;

    @Override
    public List<CalendarVo> calendarList() throws Exception {
        return calendarMapper.calendarList();
    }

    @Override
    public void calendarSave(CalendarVo vo) throws Exception {
        calendarMapper.calendarSave(vo);
    }

    @Override
    public void calendarDelete(String no) throws Exception {
        calendarMapper.calendarDelete(no);
    }

    @Override
    public void eventUpdate(CalendarVo vo) throws Exception {
        calendarMapper.eventUpdate(vo);
    }
}

 

 

 

mapper.xml - oracle(오라클)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.practice.mapper.calendar.CalendarMapper">
    
    <!-- 일정 조회하기 -->
    <select id="calendarList" resultType="com.example.practice.vo.calendar.CalendarVo">
        SELECT
            CALENDAR_NO,
            CALENDAR_TITLE AS TITLE,
            TO_CHAR(CALENDAR_START, 'YYYY-MM-DD"T"HH24:MI:SS') AS START1,
            TO_CHAR(CALENDAR_END, 'YYYY-MM-DD"T"HH24:MI:SS') AS END,
            ALL_DAY
        FROM CALENDAR
    </select>

    <!-- 일정 저장하기 -->
    <!-- 저장하고 생성된 no(key) 값을 return 시킨다 -->
    <insert id="calendarSave" parameterType="com.example.practice.vo.calendar.CalendarVo" useGeneratedKeys="true" keyColumn="CALENDAR_NO" keyProperty="calendarNo" >
        INSERT INTO CALENDAR(
            CALENDAR_NO,
            CALENDAR_TITLE,
            CALENDAR_START,
            CALENDAR_END,
            ALL_DAY
        ) values (
                         (SELECT NVL(MAX(CALENDAR_NO),  0) + 1 AS CALENDAR_NO  FROM CALENDAR),
                         #{title},
                         TO_DATE(#{start1}, 'YYYY-MM-DD HH24:MI:SS'),
                         TO_DATE(#{end}, 'YYYY-MM-DD HH24:MI:SS'),
                         #{allDay}
                 )
    </insert>

    <!-- 일정 삭제하기 -->
    <delete id="calendarDelete">
        DELETE FROM CALENDAR WHERE CALENDAR_NO = #{no}
    </delete>


    <!-- 일정 수정하기 -->
    <update id="eventUpdate" parameterType="com.example.practice.vo.calendar.CalendarVo">
        UPDATE CALENDAR SET
                            CALENDAR_TITLE = #{title} ,
                            CALENDAR_START = TO_DATE(#{start1}, 'YYYY-MM-DD"T"HH24:MI:SS') ,
                            CALENDAR_END = TO_DATE(#{end}, 'YYYY-MM-DD"T"HH24:MI:SS') ,
                            ALL_DAY = #{allDay}
        WHERE CALENDAR_NO = #{calendarNo}
    </update>
</mapper>

 

 

html
<div class="form-group">
    <div id='calendar-container'>
        <div id="calendar"></div>
    </div>
</div>

여기도 Bootstrap을 사용 중이지만

필요한 건  <div id="calendar"></div>

 

javascript
$(document).ready(function () {
    let calendarTag = $('#calendar')[0];                    // full-calendar 생성하기
    let calendar = new FullCalendar.Calendar(calendarTag, {
        height: '550px',                        // calendar 높이 설정
        expandRows: true,                       // 화면에 맞게 높이 재설정
        slotMinTime: '00:00',                   // Day 캘린더에서 시작 시간
        slotMaxTime: '23:59',                   // Day 캘린더에서 종료 시간

        customButtons: {                        // 사용자가 만드는 버튼
            testButton: {
                text: "테스트버튼"
            }
        },
        // 해더에 표시할 툴바
        headerToolbar: {                                // customButton 은 left 또는 right 안에 넣으면 적용 된다
            left: 'prevYear,prev,next,nextYear today',  // today는 , 와 띄어쓰기에 따라서 위치가 바뀐다
            // left: 'prev,next,today',
            center: 'title',
            right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
        },
        initialView: 'dayGridMonth',    // 로드 될때 캘린더 화면 기본 설정
        // initialDate: '2024-07-23',      // 초기 날짜 설정 (설정하지 않으면 오늘 날짜가 보인다.)
        navLinks: true,                 // 날짜를 선택하면 Day 캘린더나 Week 캘린더로 링크
        editable: true,                 // default : false 이벤트 드래그 등의 편집여부를 설정함
        selectable: true,               // 일자 드래그 설정
        nowIndicator: true,             // 현재 시간 마크
        dayMaxEvents: true,             // 이벤트가 많아지면 + 로 이벤트 표시
        locale: 'ko',                   // 한국어 설정
        eventAdd: function (obj) {      // 이벤트 추가 시 발생
            console.log("eventAdd : " + obj);
        },
        eventChange: function (obj) {    // 이벤트 수정 시 발생
            console.log("eventChange : " + obj);
        },
        eventRemove: function (obj) {     // 이벤트 삭제 시 발생
            console.log("eventRemove : " + obj);
        },
        select: function (arg) {          // 일자를 드래그하면 실행됨
            let title = prompt('일정 입력');
            if (title) {
                let newData = {
                        title: title,
                        start: arg.start,
                        end: arg.end,
                        allDay: arg.allDay
                }

                $.ajax({
                    url: "/calendarSave",
                    method: "POST",
                    dataType: "json",
                    data: JSON.stringify(newData),
                    contentType: 'application/json',
                    success:function (data){
                        if(data != null){
                            calendar.addEvent({
                                id: data.calendarNo,
                                title: data.title,
                                start: data.start1,
                                end: data.end,
                                allDay: data.allDay,
                                editable: true                 // default : false 이벤트 드래그 등의 편집여부를 설정함
                            });
                        }
                    }
                })
            }
            calendar.unselect()
        },
        eventClick: function (arg) {
            // 일정 클릭 시
            if (confirm("선택한 일정을 삭제하시겠습니까?")) {
                $.ajax({
                    type: "DELETE",
                    url: "/calendarDelete",
                    data: {"no" : arg.event.id},
                    success: function (data) {
                        if (data == "success") {
                            alert("삭제하였습니다.");
                            arg.event.remove();
                        }else{
                            alert("오류가 발생하였습니다");
                        }
                    }
                })
            }
        },
        eventDrop: function(arg) {              // 일정을 이동시킬때, 드래그하여 시작일과 종료일이 변경되는 경우 동작
            let event = {
                id: arg.event.id,
                title: arg.event.title,
                start1: arg.event._instance.range.start,
                end: arg.event._instance.range.end ? arg.event._instance.range.end : null,
                allDay: arg.event.allDay
            };
            $.ajax({
                url: '/eventUpdate/' + arg.event.id,
                method: 'PUT',
                contentType: 'application/json',
                data: JSON.stringify(event)
            });
        },
        eventResize: function(arg) {            // 일정의 크기를 변경시킬때 동작(일정을 늘릴때)
            let event = {
                id: arg.event.id,
                title: arg.event.title,
                start1: arg.event._instance.range.start,
                end: arg.event._instance.range.end ? arg.event._instance.range.end : null,
                allDay: arg.event.allDay
            };
            $.ajax({
                url: '/eventUpdate/' + arg.event.id,
                method: 'PUT',
                contentType: 'application/json',
                data: JSON.stringify(event)
            });
        },
        // 이벤트
        events: [
            $.ajax({
                type: "get",
                url: "/calendarList",
                success: function (data) {
                    if (data != null) {
                        for (let i = 0; i < data.length; i++) {
                            calendar.addEvent({
                                id: data[i].calendarNo,
                                title: data[i].title,
                                start: data[i].start1,
                                end: data[i].end,
                                allDay: data[i].allDay,
                                editable: true                 // default : false 이벤트 드래그 등의 편집여부를 설정함
                            })
                        }
                    }
                }
            })
        ]
    });
    // 캘린더 랜더링
    calendar.render();

});

 

 

 

 

TEST

headerToolbar 에 설정한 코드로 left center right 에 버튼이 생성된다

 

 

 

 

1. 클릭하여 일정 입력

캘린더 칸을 누루면 일정을 입력하는 팝업이 출력된다

 

 

2. 날짜를 클릭하여 시간대 설정 또는 All-Day 일정 입력

날짜를 클릭하면 Day 일정 창으로 변경되고 시간대에 클릭 또는 드래그를 하면 일정 팝업이 출력된다

 

 

 

3. 드래그하여 일정 입력

일정을 생성하려는 일자부터 끝나는 날까지 드래그를 하면 일정 팝업이 출력된다

 

 

4. 생성된 일정을 드래그하여 일정 변경(eventDrop)

일정 All-Day 바를 드래그하여 일정을 이동시킨다

 

 

5. week 에서 일정을 드래그하여 일정 생성하기

원하는 일자 및 시간에서 종료 일자 및 시간까지 드래그하여 일정을 선택한다

 

 

 

6. All-Day 일정을 드래그하여 일정일자의 크기 변경하기(eventResize)

Title 일정만 입력되어있고 시간이 없으면 All-Day 일정이다

All-Day 일정 bar 오른쪽 끝에 마우스를 가져다 대면

리사이즈 마우스 포인터로 변경되고 일정 일수를 늘리고 줄일 수 있다

 

시간대가 있는 일정은 일수를 늘릴 수 없고

시작일과 종료일만 드래그(eventDrop)로 변경 가능하다

 

 

7. 일정 삭제하기

일정 bar 를 클릭하여 삭제한다

 

 

시간대를 클릭하여 일정을 삭제한다

 

 

 

 

쉽진 않을 거라 생각했지만 이틀 내내 삽질의 삽질을 무한 반복했다

하나씩 적용해 보고 나에게 맞춰서 코드를 다시 짜고..

어느 정도 기본틀을 맞춰놓으니 이것저것 좀 더 수정하고 싶은 부분이 눈에 띄고 있으나 이 정도로만 만족해야겠다 

 

 

 

 

7. css 추가 , 이벤트에 색상 넣기

css
/* 일요일 날짜: 빨간색 */
.fc-day-sun a {
    color: red;
}

/* 토요일 날짜: 파란색 */
.fc-day-sat a {
    color: blue;
}

.fc-day-mon a{
    color: gray;
}

.fc-day-tue a{
    color: gray;
}

.fc-day-wed a {
    color: gray;
}
.fc-day-thu a {
    color: gray;
}

.fc-day-fri a {
    color: gray;
}

.fc-button-primary {
    color: red;
}

 

 

javascript
// 이벤트
events: [
    {
        title: "임직원 월례조회",
        start: ""+checkYear+"-"+ checkMonth+ "-01T09:00:00",
        end: ""+checkYear+"-"+ checkMonth+ "-01T10:00:00",
        color: "#4d4d4d",			// 이벤트 색상
        allDay: false,
        editable: false                 
    },
    {
        title: "코엑스 행사",
        start: ""+checkYear+"-"+ checkMonth+ "-10T09:00:00",
        end: ""+checkYear+"-"+ checkMonth+ "-12T18:00:00",
        color: "#e19999",			// 이벤트 색상
        textColor : "#ffffff",			// 이벤트 타이틀 색상
        allDay: false,
        editable: true                 
    },
]

 

요일의 색상과 이벤트에 색상 변화

댓글