Drag&Drop 달력 구현하기

강풍윤·2022년 10월 23일
0

현재 진행하고 있는 프로젝트에서 일자별 미리 설정한 요금유형을 날짜별로 다르게 설정해줄 수 있는 요구사항이 있었습니다. 이를 직관적이면서 사용자가 접근하기 쉽게 드래그앤 드롭으로 원하는 날짜에 요금설정을 수정, 삭제할 수 있도록 계획하게 되었습니다. 이 기능을 직접 개발하기까지 충분한 시간이 없었기 때문에 FullCalendar라는 무료 오픈소스 라이브러리(일부 서비스 유료)를 활용하여 다음과 같은 기능을 구현했습니다.

1. 구현화면

2. 미리 알아야 할 내용

3. 기본구조

기본구조는 PHP의 MVC모델을 기반으로 합니다. HTML과 JavaScript처럼 화면에서 보여지는 내용을 view로 통합하고, 데이터와 관련된 정보들을 처리하는 부분을 Model로, 그리고 Model에서 가져온 정보를 View로 어떻게 전달할지에 대한 처리를 Controller에서 구현하는 구조입니다.

4. 구현코드

4-1) HTML

<!-- FullCalendar.js 외부 스크립트 -->
<link href='/application/libraries/main.css' rel='stylesheet' />
<script src='/application/libraries/main.js'></script>
<script src='/application/libraries/ko.js'></script>

<div id="app">
	<div class="wrap">
		<div id='external-events'>
			<p style="margin-bottom: 20px;">
			<strong>요금유형</strong>
			</p>

			<?php foreach ($list as $tl) { ?>
				<div class='fc-event fc-h-event fc-daygrid-event fc-daygrid-block-event' style="padding: 10px; border: 0; color: white; background-color: <?= $tl['color'] ?>;" data-chargetypeno="<?= $tl['no'] ?>" data-typecolor="<?= $tl['color'] ?>"><?= $tl['name'] ?></div>
			<?php } ?>

			<p style="display: none;">
			<input type='checkbox' id='drop-remove' />
			<label for='drop-remove'>remove after drop</label>
			</p>
		</div>
		<div>
			<div id="calendar-wrap">
				<div id='calendar'></div>
			</div>
		</div>
	</div>
</div>

4-2) JavaScript

//------Fullcalendar JS 시작-------------------------------------------------------------------------------------------------------------------//
    var calendar =null;			

    document.addEventListener('DOMContentLoaded', function() {
        var Calendar = FullCalendar.Calendar;
        var Draggable = FullCalendar.Draggable;

        var containerEl = document.getElementById('external-events');
        var calendarEl = document.getElementById('calendar');
        var checkbox = document.getElementById('drop-remove');

        //***개발문서 주소 https://fullcalendar.io/docs ***

        // 미리 생성된 외부 이벤트, 요금유형(external events) 초기화(드래그 드롭이 가능한 이벤트)
        // -----------------------------------------------------------------
        
        //전체 이벤트 초기화
        var all_events = null;


        //DB에 있는 모든 이벤트를 가져오는 함수 getPlan() 실행
        all_events = getPlan();

        // console.log(all_events);
            
        //드래그가 가능한 이벤트(드래그를 해서 날짜에 이벤트 요소를 전달할 수 있는 외부 이벤트를 의미, extendedProps는 이벤트 요소의 dataset값으로 이벤트의 정보를 전달하는 fullcalendar에서 정의되지 않은 필드(NON-standard Fields)를 생성할 수 있음)
        new Draggable(containerEl, {
            itemSelector: '.fc-event',
            eventData: function(eventEl) {
                var dataset = eventEl.dataset;

                return {
                    title: eventEl.innerText,
                    extendedProps: {
                        chargetypeno: dataset.chargetypeno,
                        typecolor: dataset.typecolor,
                    },
                
                };
                
            }
            
        });


        // 달력 초기화
        // -----------------------------------------------------------------
        
        calendar = new Calendar(calendarEl, {
            headerToolbar: {
                left: '',
                center: 'title',
                right: 'prev,next today',
            },
            initialView: 'dayGridMonth', //달력 종류
            locale: 'ko', //달력 호환 언어설정
            events: all_events, //위에서 getPlan()로 전달된 이벤트 data를 담은 배열
            editable: false, //달력 안에서 생성된 이벤트의 수정이 가능한지 여부
            droppable: true, // this allows things to be dropped onto the calendar
            drop: function(info) {
                // is the "remove after drop" checkbox checked?
                if (checkbox.checked) {
                // if so, remove the element from the "Draggable Events" list
                info.draggedEl.parentNode.removeChild(info.draggedEl);
                }
                
            },
            //eventContent는 이벤트의 요소를 생성하는 함수
            eventContent: function (info) {
                let italicEl = document.createElement('div')
                            
                italicEl.innerHTML = '<div class="calender-droped-event" data-chargetypeno="'+info.event.extendedProps.chargetypeno+'" data-typecolor="'+info.event.extendedProps.typecolor+'">'+info.event.title+'</div>';

                let arrayOfDomNodes = [italicEl]
                return { domNodes: arrayOfDomNodes }
            },
            //*** 이벤트를 클릭했을 때 클릭한 이벤트를 컨트롤할 수 있는 실행되는 콜백함수 ***
            eventClick: function(arg) {
                if (confirm('선택하신 요금유형을 삭제하시겠습니까?')) {
                arg.event.remove();  //이벤트 삭제함수
                }
            },
            //*** 달력에 이벤트를 추가했을 때마다 추가한 이벤트를 컨트롤할 수 있는 콜백함수***
            eventReceive: function( info ) { 
                //console.log(info.event);

                var newEnvet = info.event;

                var events = new Array();
                var obj = new Object();

                obj.chargetypeno = newEnvet._def.extendedProps.chargetypeno;
                obj.start = newEnvet._instance.range.start; //요금 시작일
                events.push(obj);


                var jsondata = JSON.stringify(events);
                //console.log(jsondata);
                
                //이벤트 data를 update하는 함수를 비동기로 AJAX호출하는 함수 실행
                updatedata(jsondata);

                calendar.refetchEvents();

            },
            //*** 달력에 이벤트를 삭제했을 때마다 삭제한 이벤트를 컨트롤할 수 있는 콜백함수 ***
            eventRemove: function( removeInfo ) { 
                //console.log(removeInfo.event);

                var deleteEnvet = removeInfo.event;

                var events = new Array();

                

                var obj = new Object();

                obj.chargetypeno = deleteEnvet._def.extendedProps.chargetypeno;
                obj.start = deleteEnvet._instance.range.start; //요금 시작일
                events.push(obj);


                var jsondata = JSON.stringify(events);
                //console.log(jsondata);

                //이벤트 data를 dalete하는 함수를 비동기로 AJAX호출하는 함수 실행(*실제로 삭제하지는 않고 FK를 0으로 수정)
                deletedata(jsondata);
            }
                
        });

        //달력 렌더링
        calendar.render();

        // var alldata = null;
        // var alldata = calendar.getEvents();
        // console.log(alldata);

    });

//------AJAX함수 시작-------------------------------------------------------------------------------------------------------------------//

        //이벤트 data를 update하는 비동기로 AJAX호출하는 함수
        function updatedata(jsondata){
            $.ajax({
                type: 'POST',
                url: '/api/updateChargePlan',
                data: {"updatedata": jsondata},
                dataType: 'text',
                async: false
            })
            .done(function(result){
                //console.log(result);
                location.reload();
                alert("정상적으로 수정되었습니다");
            })
            .fail(function(request, status, error){
                alert("에러 발생" + error);
            })
        }

        //이벤트 data를 dalete하는 함수를 비동기로 AJAX호출하는 함수
        function deletedata(jsondata){
            $.ajax({
                type: 'POST',
                url: '/api/deleteChargePlan',
                data: {"deletedata": jsondata},
                dataType: 'text',
                async: true
            })
            .done(function(result){
                alert("정상적으로 삭제되었습니다");
                // location.reload();
            })
            .fail(function(request, status, error){
                alert("에러 발생" + error);
            })
        }

        //서버에 저장된 예약 데이터들을 가져오는 함수
        function getPlan(){

            var result_plan = null;

            $.ajax({
                type: 'POST',
                url: '/api/getPlan',
                data: {},
                dataType: 'json',

            })
            .done(function(result){
                result_plan = result;
            })
            .fail(function(request, status, error){
                alert("에러 발생" + error);
            })

            return result_plan;
        }
  1. FullCalendar 시작하기
  2. FullCalendar 달력 내부 이벤트 직접 정의하는 방법

4-3) API

public function updateChargePlan()
    {

        $dataArr = json_decode($_POST["updatedata"]);

        $start = strtotime($dataArr[0]->start);
        $date = date("Y-m-d", $start);
        $chargetypeno = $dataArr[0]->chargetypeno;

        $result = $this->Model->updateplan($date, $chargetypeno);
    }

    public function deleteChargePlan()
    {

        $dataArr = json_decode($_POST["deletedata"]);

        $start = strtotime($dataArr[0]->start);
        $date = date("Y-m-d", $start);
        $chargetypeno = $dataArr[0]->chargetypeno;

        $result = $this->Model->deleteplan($date, $chargetypeno);
    }

    public function getPlan()
    {

        $result = $this->Model->getplan();

        $result = array_map(function ($result) {
            return array(
                'title' => $result['name'],
                'allDay' => true,
                'start' => $result['date'],
                'color' => $result['color'],
                'typecolor' => $result['color'],
                'chargetypeno' => $result['charge_type_no']
            );
        }, $result);

        print_r(json_encode($result, JSON_UNESCAPED_UNICODE));

        return $result;
    }
  1. REST API란

4-4) Model

function updateplan($date, $typeno)
	{
		$sql = "UPDATE tableA SET charge_type_no = $typeno WHERE date = '$date'";

		$result = $this->db->query($sql);
		return $result;
	}

function deleteplan($date, $typeno)
	{

		$sql = "UPDATE tableA SET charge_type_no = 0 WHERE date = '$date'";

		$result = $this->db->query($sql);
		return $result;
	}
function getplan()
	{
		$sql = "SELECT A.charge_type_no, A.date, B.name, B.color 
		FROM tableA A
		LEFT JOIN tableB B 
		ON A.charge_type_no = B.no
		where A.date between '2022-01-01' and '2022-12-31'
		";

		$result = $this->db->query($sql)->result_array();

		return $result;
	}
profile
https://github.com/KANGPUNGYUN

0개의 댓글