News

Calendar 실수 3가지 — 유아이파이브 달력 완성 #shorts #SAP #UI5

▶ YouTube에서 보기

1. 개요 및 이 글의 목표

SAPUI5에서 날짜를 다루는 컨트롤은 여러 가지가 있지만, 그중에서도 sap.ui.unified.Calendar는 단순한 입력 필드가 아닌 전체 달력 뷰를 화면에 펼쳐 보여주는 컨트롤입니다. 예약 시스템, 휴가 신청, 일정 관리 화면처럼 사용자가 한 달 전체를 한눈에 보면서 날짜를 클릭해야 하는 시나리오에서 핵심적으로 사용됩니다.

본 글에서 다루는 내용 체크리스트:

  • sap.ui.unified.Calendar의 구조와 sap.m.DatePicker와의 차이
  • XML View 선언과 startDate 초기값 설정
  • 단일 날짜 선택, 범위 선택(intervalSelection), 복수 선택(multiSelection) 구현
  • select 이벤트 핸들러에서 getSelectedDates() 활용
  • specialDates를 활용한 휴일/공휴일 마킹
  • JavaScript Date 객체와 UI5 DateFormat 변환 시 자주 발생하는 함정

2. 핵심 개념 — sap.ui.unified.Calendar vs sap.m.DatePicker 차이

SAPUI5에는 날짜 입력을 위한 다양한 컨트롤이 존재합니다. 가장 자주 혼동되는 두 컨트롤이 sap.m.DatePickersap.ui.unified.Calendar입니다. 두 컨트롤은 모두 날짜를 다루지만, 사용 목적과 UX가 명확히 다릅니다.

sap.m.DatePicker는 본질적으로 입력 필드(Input)입니다. 폼 안에 들어가 "생년월일", "주문일" 같은 필드를 처리할 때 적합하며, 사용자가 필드를 탭하면 작은 팝업 달력이 열렸다가 선택 후 닫힙니다. 입력값은 문자열로 폼 모델에 바인딩되는 경우가 일반적입니다.

반면 sap.ui.unified.Calendar항상 펼쳐져 있는 풀 캘린더 뷰입니다. 별도의 트리거 없이 화면의 한 영역을 차지하며, 월/주/일 단위 그리드로 표시됩니다. 다음과 같은 특징을 가집니다.

  • 여러 날짜를 한 번에 선택할 수 있는 singleSelection, intervalSelection 모드 제공
  • selectedDates 집계(aggregation)를 통해 선택된 날짜 목록을 DateRange 객체로 관리
  • specialDates 집계로 공휴일, 회의일 등 특별한 날을 색상/타입으로 마킹
  • startDate 속성으로 초기에 보여줄 달의 기준일 지정

비유하자면 DatePicker가 "필요할 때만 꺼내는 손목시계"라면, Calendar는 "벽에 걸어둔 큰 달력"입니다. 이 글에서는 후자에 집중합니다. sap.ui.unified 라이브러리를 사용하므로 매니페스트(manifest.json)의 sap.ui5.dependencies.libs에 라이브러리를 추가하는 것이 권장됩니다.

3. 1단계: sap.ui.unified.Calendar 뷰 선언과 startDate 초기 설정

가장 단순한 형태부터 시작합니다. XML View에 캘린더 컨트롤을 선언하고, 컨트롤러에서 초기 표시 월을 지정합니다.

XML View 선언

<mvc:View
    controllerName="com.example.cal.controller.Main"
    xmlns="sap.m"
    xmlns:mvc="sap.ui.core.mvc"
    xmlns:u="sap.ui.unified">
    <Page title="Calendar 기본 예제">
        <content>
            <u:Calendar
                id="basicCal"
                select="onDateSelect"
                startDateChange="onStartDateChange"/>
        </content>
    </Page>
</mvc:View>

manifest.json 의존성 추가

{
  "sap.ui5": {
    "dependencies": {
      "libs": {
        "sap.m": {},
        "sap.ui.unified": {}
      }
    }
  }
}

Controller에서 startDate 설정

sap.ui.define([
    "sap/ui/core/mvc/Controller",
    "sap/ui/core/format/DateFormat",
    "sap/m/MessageToast"
], function (Controller, DateFormat, MessageToast) {
    "use strict";

    return Controller.extend("com.example.cal.controller.Main", {
        onInit: function () {
            var oCalendar = this.byId("basicCal");
            // 2026년 6월을 초기 표시 월로 지정
            oCalendar.setStartDate(new Date(2026, 5, 1));
        },

        onDateSelect: function (oEvent) {
            var oCalendar = oEvent.getSource();
            var aSelected = oCalendar.getSelectedDates();
            if (aSelected.length === 0) {
                return;
            }
            var oDate = aSelected[0].getStartDate();
            var oFmt = DateFormat.getDateInstance({ pattern: "yyyy-MM-dd" });
            MessageToast.show("선택된 날짜: " + oFmt.format(oDate));
        },

        onStartDateChange: function (oEvent) {
            // 사용자가 월을 이동했을 때 호출됨
            var oNewStart = oEvent.getSource().getStartDate();
            console.log("표시 월 변경:", oNewStart);
        }
    });
});

핵심 포인트는 startDate가 "선택된 날짜"가 아니라 "달력이 어느 달을 펼쳐 보여줄지"를 결정한다는 점입니다. JavaScript Date 생성자의 월 인덱스는 0부터 시작하므로 new Date(2026, 5, 1)은 2026년 6월 1일입니다. 이 부분에서 한 달 어긋나는 실수가 자주 발생합니다.

4. 2단계: intervalSelection으로 날짜 범위 선택 구현

호텔 예약처럼 시작일과 종료일을 한 쌍으로 선택해야 한다면 intervalSelection="true"를 사용합니다. 사용자가 첫 클릭으로 시작일을, 두 번째 클릭으로 종료일을 지정합니다.

XML View

<u:Calendar
    id="rangeCal"
    intervalSelection="true"
    singleSelection="true"
    select="onRangeSelect"/>
<Text id="rangeText" text="범위를 선택하세요"/>

Controller에서 범위 처리

onRangeSelect: function (oEvent) {
    var oCalendar = oEvent.getSource();
    var aSelected = oCalendar.getSelectedDates();
    var oText = this.byId("rangeText");
    var oFmt = sap.ui.core.format.DateFormat.getDateInstance({
        pattern: "yyyy.MM.dd"
    });

    if (aSelected.length === 0) {
        oText.setText("선택된 범위 없음");
        return;
    }

    var oRange = aSelected[0];
    var oStart = oRange.getStartDate();
    var oEnd = oRange.getEndDate();

    if (!oEnd) {
        // 사용자가 시작일만 클릭한 상태
        oText.setText("시작일: " + oFmt.format(oStart) + " (종료일 대기 중)");
        return;
    }

    // 일수 차이 계산 (밀리초 -> 일 단위)
    var iDays = Math.round((oEnd - oStart) / (1000 * 60 * 60 * 24)) + 1;
    oText.setText(
        oFmt.format(oStart) + " ~ " + oFmt.format(oEnd) +
        " (총 " + iDays + "일)"
    );
}

주의할 점은 intervalSelection 모드에서도 selectedDates 배열의 길이는 항상 1이라는 점입니다. 하나의 DateRange 객체가 startDateendDate를 모두 담습니다. 사용자가 첫 번째 클릭만 한 상태에서는 endDateundefined이므로 반드시 null 체크가 필요합니다.

5. 3단계: multiSelection으로 복수 날짜 선택

직원 근무 스케줄링이나 불규칙한 회의 일자 지정처럼 연속되지 않은 여러 날짜를 선택해야 할 때는 singleSelection="false"로 설정하여 다중 선택 모드를 활성화합니다.

XML View

<u:Calendar
    id="multiCal"
    singleSelection="false"
    intervalSelection="false"
    select="onMultiSelect"/>
<List id="selectedList" headerText="선택된 날짜 목록"
      items="{/dates}">
    <StandardListItem title="{date}" description="{weekday}"/>
</List>

Controller — JSONModel로 결과 바인딩

onInit: function () {
    var oModel = new sap.ui.model.json.JSONModel({ dates: [] });
    this.getView().setModel(oModel);
},

onMultiSelect: function (oEvent) {
    var aSelected = oEvent.getSource().getSelectedDates();
    var oFmt = sap.ui.core.format.DateFormat.getDateInstance({
        pattern: "yyyy-MM-dd (EEE)"
    });
    var aResult = aSelected.map(function (oRange) {
        var oDate = oRange.getStartDate();
        return {
            date: oFmt.format(oDate),
            weekday: ["일","월","화","수","목","금","토"][oDate.getDay()] + "요일"
        };
    });
    this.getView().getModel().setProperty("/dates", aResult);
}

다중 선택 모드에서는 selectedDates 배열에 사용자가 클릭한 모든 날짜가 각각 별도의 DateRange로 누적됩니다. 동일한 날짜를 다시 클릭하면 해당 항목이 배열에서 제거됩니다(토글 동작). 따라서 매번 select 이벤트에서 전체 배열을 다시 읽어 모델을 갱신하는 패턴이 안전합니다.

6. select 이벤트 핸들러와 getSelectedDates() 활용

세 가지 모드 모두에서 핵심 진입점은 select 이벤트와 getSelectedDates() 메서드입니다. 이벤트 매개변수 객체에는 클릭된 단일 날짜 정보가 직접 들어 있지 않다는 점이 초보자가 가장 자주 막히는 부분입니다.

패턴: 이벤트 → 캘린더 → DateRange 목록

onSelect: function (oEvent) {
    // 1단계: 이벤트 소스에서 캘린더 인스턴스 획득
    var oCalendar = oEvent.getSource();

    // 2단계: 선택된 DateRange 배열 조회
    var aSelectedDates = oCalendar.getSelectedDates();

    // 3단계: 각 DateRange에서 실제 Date 객체 추출
    aSelectedDates.forEach(function (oRange) {
        var oStart = oRange.getStartDate();   // JavaScript Date 객체
        var oEnd   = oRange.getEndDate();     // 범위 모드일 때만 값 존재
        console.log("선택:", oStart, "~", oEnd);
    });
}

UTC vs 로컬 타임 함정

getStartDate()가 반환하는 Date 객체는 로컬 타임 기준입니다. 그러나 OData 서비스로 전송할 때는 보통 UTC ISO 문자열이 요구됩니다. 변환 시 시차로 인해 하루가 어긋나는 사례가 흔합니다.

// 잘못된 예: 로컬 -> ISO 변환 시 시차 발생
var sIso = oDate.toISOString(); // "2026-06-03T15:00:00.000Z" 같은 결과

// 권장: 날짜만 다룰 때는 UTC 자정으로 정규화
var oUtcDate = new Date(Date.UTC(
    oDate.getFullYear(),
    oDate.getMonth(),
    oDate.getDate()
));

// 또는 DateFormat의 UTC 옵션 사용
var oFmt = sap.ui.core.format.DateFormat.getDateInstance({
    pattern: "yyyy-MM-dd",
    UTC: true
});

7. 자주 겪는 함정 — selectedDates 바인딩 오류, 날짜 포맷 변환

FAQ 1: selectedDates를 JSONModel에 바인딩하면 표시가 안 돼요

selectedDatesDateRange 객체의 집계(aggregation)입니다. JSON 배열을 직접 바인딩하려면 템플릿으로 sap.ui.unified.DateRange를 사용해야 하며, JSON 안의 날짜는 문자열이 아니라 Date 객체여야 합니다. 문자열로 저장하면 표시되지 않거나 잘못된 날짜가 강조됩니다.

<u:Calendar
    selectedDates="{
        path: '/selections',
        templateShareable: false
    }">
    <u:selectedDates>
        <u:DateRange startDate="{start}" endDate="{end}"/>
    </u:selectedDates>
</u:Calendar>

FAQ 2: 캘린더가 화면에 안 뜨고 빈 영역만 보입니다

sap.ui.unified 라이브러리가 로드되지 않은 경우입니다. manifest.jsonlibs"sap.ui.unified": {}를 추가하거나, 부트스트랩 스크립트의 data-sap-ui-libs에 명시해야 합니다. 빌드 후에도 누락되어 있으면 콘솔에 sap.ui.unified.Calendar is not a function 류의 에러가 표시됩니다.

FAQ 3: 일부 모바일 브라우저에서 클릭이 안 됩니다

sap.ui.unified.Calendar는 일반적으로 데스크톱 시나리오에 더 적합하다는 평가가 있습니다. 모바일에서 폼 입력 용도라면 sap.m.DatePicker 또는 sap.m.DateRangeSelection이 권장됩니다. 풀 캘린더가 꼭 필요한 모바일 시나리오라면 컨테이너 폭을 충분히 확보하고 singleSelection 모드로 단순화하는 것이 좋습니다.

FAQ 4: 두 번째 캘린더 인스턴스가 첫 번째 선택을 공유합니다

같은 JSONModel 경로를 두 캘린더가 공유 바인딩하는 경우 발생합니다. 각 캘린더마다 별도의 모델 경로를 두거나 templateShareable: false를 명시하여 템플릿 인스턴스 공유를 차단해야 합니다.

8. 응용 패턴 — 휴일 마킹, specialDates, 주 단위 선택

실무에서는 단순 선택을 넘어 "공휴일은 빨강", "회의일은 파랑"처럼 시각적 구분이 필요합니다. specialDates 집계와 CalendarDayType 열거형을 사용합니다.

specialDates로 공휴일 마킹

<u:Calendar id="holidayCal">
    <u:specialDates>
        <u:DateTypeRange
            startDate="2026-06-06"
            type="Type01"
            tooltip="현충일"/>
        <u:DateTypeRange
            startDate="2026-08-15"
            type="Type01"
            tooltip="광복절"/>
        <u:DateTypeRange
            startDate="2026-12-25"
            type="Type02"
            tooltip="크리스마스"/>
    </u:specialDates>
</u:Calendar>

JSONModel로 동적 공휴일 로드

onInit: function () {
    var oModel = new sap.ui.model.json.JSONModel();
    oModel.loadData("/api/holidays/2026")
        .then(function () {
            var oCal = this.byId("holidayCal");
            oModel.getProperty("/list").forEach(function (oHoliday) {
                oCal.addSpecialDate(new sap.ui.unified.DateTypeRange({
                    startDate: new Date(oHoliday.date),
                    type: oHoliday.isNational
                          ? "Type01"   // 국경일: 보통 빨강 계열
                          : "Type07",  // 회사 기념일: 다른 색
                    tooltip: oHoliday.name
                }));
            });
        }.bind(this));
    this.getView().setModel(oModel, "holiday");
}

주 단위 선택 패턴

사용자가 한 날짜를 클릭하면 자동으로 해당 주(월~일)를 통째로 선택하도록 만들 수 있습니다. intervalSelection 모드와 select 이벤트를 조합합니다.

onWeekSelect: function (oEvent) {
    var oCal = oEvent.getSource();
    var aSel = oCal.getSelectedDates();
    if (aSel.length === 0) return;

    var oClicked = aSel[0].getStartDate();
    var iDay = oClicked.getDay(); // 0=일, 1=월, ...
    var iDiffToMonday = iDay === 0 ? -6 : 1 - iDay;

    var oMonday = new Date(oClicked);
    oMonday.setDate(oClicked.getDate() + iDiffToMonday);
    var oSunday = new Date(oMonday);
    oSunday.setDate(oMonday.getDate() + 6);

    oCal.removeAllSelectedDates();
    oCal.addSelectedDate(new sap.ui.unified.DateRange({
        startDate: oMonday,
        endDate: oSunday
    }));
}

이 패턴은 주차 단위 매출 보고서, 주간 근무표 등에서 활용도가 높습니다. 다음 단계로는 sap.ui.unified.CalendarLegend를 추가하여 색상별 의미를 사용자에게 명시하고, OData V4 모델과 결합하여 서버에서 받은 일정 데이터를 실시간 반영하는 패턴으로 확장할 수 있습니다. 또한 단순한 날짜 선택을 넘어 일정 블록까지 표시해야 한다면 sap.m.PlanningCalendar 같은 상위 컨트롤로 이행하는 것이 일반적인 경로입니다.

참고 자료

  • SAPUI5 API Reference: sap.ui.unified.Calendar (help.sap.com 내 SAPUI5 SDK)
  • SAPUI5 Sample: Calendar Single Day Selection (sapui5.hana.ondemand.com 데모킷)
  • SAPUI5 API Reference: sap.ui.unified.DateRange / DateTypeRange
  • SAPUI5 Developer Guide: Date and Time Controls 개요 (help.sap.com)
  • SAPUI5 API Reference: sap.ui.core.format.DateFormat
  • SAP Community Blog: Working with sap.ui.unified.Calendar specialDates

댓글 0

아직 댓글이 없습니다.