이전에 쓰던 쓰던 일정 프로그램을 개선 시키고자 fullcalendar로 일정 프로그램을 새로 만들었다.
ajax, jquery, java, oracle 모두 사용하여 DB에 저장 시키는 것까지 모두 구현 했고, fullcalendar의 일정 외에 분류, 구분 값들을 추가하여 일정을 관리 할 수 있도록 했다.
* 중요 내용이 아닌 것들은 생략
1. Schedule.jsp (script 영역)
var sch;
var stdDate;
$().ready(function() {
// 사이트 변경으로 인한 일정 조회
$(document).on('change', '[name=SITE_NO]', function(){
fn_schMonthList();
});
// 구분 값 변경으로 인한 일정 조회
$(document).on('click', '[name=SCHEDULE_GBN_ARR]', function(){
fn_schMonthList();
});
// 초기세팅
fn_init();
});
function fn_init(){
const currentDate = new Date();
// 날짜 정보 가져오기
var year = currentDate.getFullYear(); // 연도
var month = currentDate.getMonth() + 1; // 월 (0부터 시작하므로 +1 필요)
if(month < 10) month = "0" + month;
var day = currentDate.getDate(); // 일
if(day < 10) day = "0" + day;
stdDate = year + "-" + month + "-" + day;
$("#sendForm [name=SEARCH_YEAR]").val(year);
$("#sendForm [name=SEARCH_MONTH]").val(month);
fn_scheduleGbnList();
}
//DB에서 구분값 리스트 불러와
function fn_scheduleGbnList(){
//ajax...
fn_schMonthList();
}
//DB에서 일정 리스트 조회
function fn_schMonthList(){
//ajax...
sch = data;
fn_setSchedule();
}
//캘린더 생성
function fn_setSchedule(){
var calendarEl = document.getElementById('calendar');
var calendar = new FullCalendar.Calendar(calendarEl, {
initialDate: stdDate, /* 기본일자 세팅 */
locale: 'ko',
timeZone: 'UTC',
height: 750,
eventTimeFormat: { /* 일정 시간 표기 방법 설정 */
hour: 'numeric',
minute: '2-digit',
meridiem: 'short'
},
/* 버튼 이벤트를 위해 커스텀 버튼 생성 */
customButtons: {
todayCustomButton: {
text: '오늘',
click: function() {
fn_init();
}
},
prevCustomButton: {
icon : 'chevron-left',
click: function() {
fn_moveSchedule('prev');
}
},
nextCustomButton: {
icon : 'chevron-right',
click: function() {
fn_moveSchedule('next');
}
}
},
headerToolbar: {
left: 'todayCustomButton dayGridMonth,timeGridWeek,timeGridDay,listMonth',
center: 'prevCustomButton,title,nextCustomButton',
right: '',
},
allDayText : '종일',
buttonText: {
today: '오늘',
month: '월간',
week: '주간',
day: '일간',
list: '목록',
},
selectable: true,
/* navLinks: true, 일자 클릭 시 일자내 시간 스케줄 화면 이동 */
dayMaxEvents: true,
editable: true,
noEventsContent: '일정 없음',
eventOrder: ['allDay', 'start', 'end'], /* 일정 순서 설정 */
eventResize: function(e){ /* 일정 드래그 리사이즈 이벤트 */
var startDayStr = e.event.startStr;
var endDayStr = e.event.endStr;
if(endDayStr == ''){
endDayStr = startDayStr;
}else{
if(e.event.allDay){
var endDate = new Date(endDayStr);
endDate.setDate(endDate.getDate() - 1);
endDayStr = endDate.toISOString().slice(0, 10);
}
}
startDayStr = fn_formatDateString(startDayStr);
endDayStr = fn_formatDateString(endDayStr);
fn_scheduleChangeDay(startDayStr, endDayStr, e.event.id);
},
eventDrop: function(e){ /* 일정 드래그 드롭 이벤트 */
console.log(e);
var startDayStr = e.event.startStr;
var endDayStr = e.event.endStr;
if(endDayStr == ''){
endDayStr = startDayStr;
}else{
if(e.event.allDay){
var endDate = new Date(endDayStr);
endDate.setDate(endDate.getDate() - 1);
endDayStr = endDate.toISOString().slice(0, 10);
}
}
startDayStr = fn_formatDateString(startDayStr);
endDayStr = fn_formatDateString(endDayStr);
fn_scheduleChangeDay(startDayStr, endDayStr, e.event.id);
},
eventClick: function(e) { /* 일정 클릭 이벤트 */
var startDayStr = e.event.startStr;
var endDayStr = e.event.endStr;
if(endDayStr == ''){
endDayStr = startDayStr;
}else{
var endDate = new Date(endDayStr);
endDate.setDate(endDate.getDate() - 1);
endDayStr = endDate.toISOString().slice(0, 10);
}
startDayStr = fn_formatDateString(startDayStr);
endDayStr = fn_formatDateString(endDayStr);
fn_schedulePop(startDayStr, endDayStr, e.event.id);
},
select: function(e) { /* 빈 일정 영역 클릭 이벤트 */
var endDayStr = e.endStr;
var endDate = new Date(endDayStr);
endDate.setDate(endDate.getDate() - 1);
endDayStr = endDate.toISOString().slice(0, 10);
fn_schedulePop(e.startStr, endDayStr);
},
events: sch /* DB에서 받아온 일정 리스트 */
});
calendar.render();
}
function stringFormat(p_val){
if(p_val < 10){
return p_val = '0'+p_val;
}else{
return p_val;
}
}
// 일정 시간을 fullcalendar timeZone 형식에 맞게 변경
function fn_formatDateString(dateStr){
if(dateStr.includes('T')){
inputDate = new Date(dateStr);
}else{
inputDate = new Date(dateStr + 'T00:00:00Z');
}
const year = inputDate.getUTCFullYear();
const month = String(inputDate.getUTCMonth() + 1).padStart(2, '0');
const day = String(inputDate.getUTCDate()).padStart(2, '0');
const formattedDate = year + '-' + month + '-' + day;
return formattedDate;
}
//일정 변경
function fn_scheduleChangeDay(startDay, endDay, scheduleSeq){
fn_comm_ajax({
url : "/scheduleChangeDay.do",
data : {START_DAY:startDay, END_DAY:endDay, SCHEDULE_SEQ:scheduleSeq},
dataType : "json",
success : function(data) {
if(null != data){
// alert(data.MSG);
if(data.RESULT == 'SUCCESS'){
//화면 상 이동이 되어 있어 리로드 불필요
// parent.fn_schMonthList();
}
}else{
alert('변경에 실패했습니다.');
}
}
});
}
// 일정 월 이동
function fn_moveSchedule(gbn){
var searchYear = $("#sendForm [name=SEARCH_YEAR]").val();
var searchMonth = $("#sendForm [name=SEARCH_MONTH]").val();
if(gbn == 'prev'){
var searchDay = new Date(searchYear, parseInt(searchMonth)-2, 1);
}else{
if(searchMonth == 11){
var searchDay = new Date(searchYear, parseInt(searchMonth), 1);
}else{
var searchDay = new Date(searchYear, parseInt(searchMonth), 1);
}
}
var year = searchDay.getFullYear(); // 연도
var month = searchDay.getMonth()+1; // 월 (0부터 시작하므로 +1 필요)
if(month < 10) month = "0" + month;
$("#sendForm [name=SEARCH_YEAR]").val(year);
$("#sendForm [name=SEARCH_MONTH]").val(month);
stdDate = year + "-" + month + "-01";
fn_schMonthList();
}
// 일정 팝업 생성
function fn_schedulePop(startDt, endDt, scheduleSeq){
if(endDt == '' || endDt == 'undefined') endDt = startDt;
if(scheduleSeq == 'undefined') scheduleSeq = '';
$("#sendForm [name=SCHEDULE_SEQ]").val(scheduleSeq);
$("#sendForm [name=START_DAY]").val(startDt);
$("#sendForm [name=END_DAY]").val(endDt);
//팝업 생성
}
//일정 구분 관리 팝업 생성
function scheduleGbnPop() {
$("body").css("overflow-y","hidden");
//팝업 생성
}
//팝업 종료
function ifmPopClose(){
$("body").css("overflow-y","");
$("#iframePop").remove();
fn_scheduleGbnList();
}
fullcalendar의 이벤트에서 일정 정보를 받으면 이벤트에 들어 있는 종료일자가 하루 더 포함되어 나온다..
이벤트 객체 정보를 확인해 날짜에 맞춰 세팅 했다.
1. Schedule.jsp (html영역)
<style>
.calender_wrap {display:flex;}
.calender_wrap .listBox {width:250px;margin-right: 15px;border:1px solid #dde2ea;overflow-y: auto;border-top:0px;}
.calender_wrap .listBox .scroll_list {height:auto;overflow: hidden;}
.calender_wrap .listBox .scroll_list ul {overflow-y: auto;}
.calender_wrap .listBox ul>li {border-bottom: 20px;border-bottom:1px solid #dde2ea}
.calender_wrap .listBox ul>li:nth-child(odd) {background:#f6f7fb}
.calender_wrap .listBox ul>li {display:block;padding: 10px;}
.calender_wrap #calendar {width:calc(100% - 265px)}
.fc-header-toolbar {margin-top: 15px;}
.fc-toolbar-title {display:inline-block;vertical-align:middle;margin-left:15px !important;margin-right:15px !important;}
.fc .fc-more-popover .fc-popover-body {max-height:150px;overflow-y:auto}
.calendarDiv.colorBox{width: 20px; height: 20px; float: left; margin-top: 10px; margin-left: 10px; margin-right: 10px;}
.fc-event-title, .fc-event-time {color:#fff;}
</style>
<div class="tabs">
<div id="program_schedule_head">
<form id="sendForm" name="sendForm" method="post">
<input type="hidden" name="SCHEDULE_SEQ"/>
<input type="hidden" name="START_DAY"/>
<input type="hidden" name="END_DAY"/>
<input type="hidden" name="SEARCH_YEAR"/>
<input type="hidden" name="SEARCH_MONTH"/>
<div class="search_div">
<div style="float:left;">
<!-- 사이트 리스트 -->
<select name="SITE_NO" class="select" style="width:130px; margin-top:5px;">
<c:set var="gbn1" value=""/>
<c:forEach var="list" items="${schGroup }" varStatus="attr">
<c:if test="${attr.first }">
<c:set var="gbn1" value="${list.SITE_GROUP_NO }"/>
<optgroup label="${list.SITE_GROUP_NM }">
</c:if>
<c:if test="${gbn1 ne list.SITE_GROUP_NO }">
<c:set var="gbn1" value="${list.SITE_GROUP_NO }"/>
</optgroup>
<optgroup label="${list.SITE_GROUP_NM }">
</c:if>
<option value="${list.SITE_NO }">${list.SITE_NM }</option>
<c:if test="${attr.last }">
</optgroup>
</c:if>
</c:forEach>
</select>
</div>
<p style="float:right;">
<a href="#;" class="btn_icon" onclick="scheduleGbnPop();" style="height:30px; line-height: 31px;">일정구분관리</a>
</p>
</div>
<div class="calender_wrap">
<div class="listBox">
<div class="scroll_list" style="margin-top:0px;">
<ul id="gbnList">
</ul>
</div>
</div>
<div id='calendar'></div>
</div>
</form>
</div>
Java, SQL은 중요한 부분만 정리
3. Java에서 일정 데이터 포맷 변경
/*
* 일정 데이터를 fullcalendar 데이터 형식에 맞게 변경
*/
@SuppressWarnings("unchecked")
public JSONArray changeScheduleJsonData(List<Map<String, Object>> list) throws Exception {
JSONArray jsonArr = new JSONArray();
if(null != list){
for(int idx=0; idx<list.size(); idx++){
JSONObject map = new JSONObject();
map.put("id", list.get(idx).get("SCHEDULE_SEQ"));
map.put("title", list.get(idx).get("SUBJECT"));
map.put("color", list.get(idx).get("COLOR_CD"));
if("Y".equals(list.get(idx).get("TIME_YN"))){
// map.put("allDay", "N");
map.put("start", list.get(idx).get("START_DT"));
map.put("end", list.get(idx).get("END_DT"));
}else{
// map.put("allDay", "Y");
map.put("start", list.get(idx).get("START_DAY"));
map.put("end", list.get(idx).get("END_DAY"));
}
jsonArr.add(map);
}
}
return jsonArr;
}
4. DB 일정 데이터 삽입
<selectKey order="BEFORE" keyProperty="SCHEDULE_SEQ" resultType="java.lang.Integer">
SELECT NVL(MAX(SCHEDULE_SEQ)+1,1) FROM CMS_SCHEDULE
</selectKey>
INSERT INTO CMS_SCHEDULE(SCHEDULE_SEQ, SITE_NO, SCHEDULE_GBN_SEQ, TIME_YN
, START_DAY, END_DAY, STD_DAY
, SUBJECT, CONTENTS, PLACE
, USE_YN, REG_DATE, REG_USER_ID, REG_IP)
VALUES(
#{SCHEDULE_SEQ}
, #{SITE_NO}
, #{SCHEDULE_GBN_SEQ}
, NVL(#{TIME_YN}, 'N')
, TO_DATE(#{START_DAY} || ' ' || NVL(#{START_HOUR}, '00') || ':' || NVL(#{START_MINUTE}, '00') || ':00', 'YYYY-MM-DD HH24:MI:SS')
, TO_DATE(#{END_DAY} || ' ' || NVL(#{END_HOUR}, '00') || ':' || NVL(#{END_MINUTE}, '00') || ':00', 'YYYY-MM-DD HH24:MI:SS')
, TO_CHAR(TO_DATE(#{START_DAY}, 'YYYY-MM-DD HH24:MI:SS') + (TO_DATE(#{END_DAY}, 'YYYY-MM-DD HH24:MI:SS') - TO_DATE(#{START_DAY}, 'YYYY-MM-DD HH24:MI:SS')) / 2, 'YYYYMMDD')
, #{SUBJECT}
, #{CONTENTS}
, #{PLACE}
, 'Y'
, #{REG_DT}
, #{REG_ID}
, #{REG_IP}
)
5. DB 일정 데이터 조회
SELECT T1.SCHEDULE_SEQ
, T1.SITE_NO
, T1.SCHEDULE_GBN_SEQ
, T1.TIME_YN
, TO_CHAR(T1.START_DAY, 'YYYY-MM-DD"T"HH24:MI:SS') AS START_DT
, TO_CHAR(T1.START_DAY, 'YYYY-MM-DD') AS START_DAY
, TO_CHAR(T1.START_DAY, 'HH24') AS START_HOUR
, TO_CHAR(T1.START_DAY, 'MI') AS START_MINUTE
, CASE WHEN T1.TIME_YN = 'Y' THEN TO_CHAR(T1.END_DAY, 'YYYY-MM-DD"T"HH24:MI:SS')
ELSE TO_CHAR(T1.END_DAY+1, 'YYYY-MM-DD"T"HH24:MI:SS')
END AS END_DT
, CASE WHEN T1.TIME_YN = 'Y' THEN TO_CHAR(T1.END_DAY, 'YYYY-MM-DD')
ELSE TO_CHAR(T1.END_DAY+1, 'YYYY-MM-DD')
END AS END_DAY
, TO_CHAR(T1.END_DAY, 'HH24') AS END_HOUR
, TO_CHAR(T1.END_DAY, 'MI') AS END_MINUTE
, T1.SUBJECT
, T2.COLOR_CD
FROM CMS_SCHEDULE T1
JOIN CMS_SCHEDULE_GBN_CONFIG T2
ON T1.SCHEDULE_GBN_SEQ = T2.SCHEDULE_GBN_SEQ
AND T2.USE_YN = 'Y'
WHERE T1.SITE_NO = #{SITE_NO}
AND T1.STD_DAY BETWEEN TO_CHAR(TO_DATE(#{SEARCH_YEAR} || '-' || #{SEARCH_MONTH} || '-01', 'YYYY-MM-DD') - 15, 'YYYYMMDD')
AND TO_CHAR(LAST_DAY(TO_DATE(#{SEARCH_YEAR} || '-' || #{SEARCH_MONTH} || '-01', 'YYYY-MM-DD')) + 15, 'YYYYMMDD')
AND T1.USE_YN = 'Y'
<choose>
<when test="SCHEDULE_GBN_ARR != null and SCHEDULE_GBN_ARR != ''">
AND T1.SCHEDULE_GBN_SEQ IN
<foreach collection="SCHEDULE_GBN_ARR" item="arr" open="(" close=")" separator=",">
#{arr}
</foreach>
</when>
<otherwise>
AND 1=2
</otherwise>
</choose>
캘린더에서 이전,다음 월의 일자가 노출 되는게 있어 어떻게 가져올지 고민 하다가 선택 월의 앞뒤로 3달을 조회 하려고 했는데 데이터 건수가 몇천, 몇만으로 넘어가면 조회 속도가 감당이 안될 것 같아 고민을 하다가,
insert 시 시작, 종료 일의 중간 값을 넣어 해당 값을 기준으로 조회 하기로 했다.
- 인덱스는 (SITE_NO, STD_DAY)로 추가
100만건 기준으로 약 10ms 나오는 듯..
연도별 조회로 하면
#{SEARCH_YEAR} BETWEEN TO_CHAR(T1.START_DAY, 'YYYY') AND TO_CHAR(T1.END_DAY, 'YYYY')
위 조건으로 조회하면 될 것 같음.
완성 화면
- 공식 문서
FullCalendar - JavaScript Event Calendar
Open Source... With over 10 years of open source and over 120 contributors, FullCalendar will always have a free and open source core. Learn more
fullcalendar.io