문제 상황

주소 입력 필드처럼, 직접 타이핑은 막고 주소 검색 팝업으로만 값을 선택하게 해야 하는 경우가 있다.

editable="false"를 쓰면 가장 쉬운 방법이지만, UI가 회색으로 비활성화되어 보이는 문제가 있다.
readonly 속성은 값 세팅 시점에 따라 의도치 않게 동작하기도 한다.


해결 방법 — initReadInput

핵심 아이디어:
포커스가 잡히는 순간 즉시 blur()를 호출해서 키보드 입력을 원천 차단.


전체 코드

/**
 * Suggestion 없는 Input 필드를 읽기전용으로 처리
 * - editable=false처럼 UI가 비활성화되어 보이지 않고,
 *   겉모습은 일반 Input이지만 타이핑이 불가능한 상태
 *
 * @param {object} that   - Controller의 this
 * @param {string|string[]} inputArry - Input ID (단일 string 또는 string 배열)
 *
 * 사용법:
 *   CommonSuggestions.initReadInput(this, "addressInput");
 *   CommonSuggestions.initReadInput(this, ["addressInput", "detailAddressInput"]);
 */
initReadInput: function(that, inputArry) {
    if (!Array.isArray(inputArry)) {
        inputArry = [inputArry];
    }
    inputArry.forEach(function(inputId) {
        var oInput = getById(that, inputId);
        if (!oInput) return;

        if (!oInput.getShowSuggestion || !oInput.getShowSuggestion()) {
            oInput.addEventDelegate({
                onAfterRendering: function() {
                    var $input = oInput.$().find("input");
                    $input.off("focus.readonly");
                    $input.on("focus.readonly", function() {
                        this.blur();
                    });
                }
            });
        }
    });
}

코드 포인트

1. 단일 ID / 배열 ID 모두 지원

if (!Array.isArray(inputArry)) {
    inputArry = [inputArry];
}

문자열 하나를 넘겨도, 배열로 넘겨도 동일하게 처리.


2. Suggestion 없는 Input에만 적용

if (!oInput.getShowSuggestion || !oInput.getShowSuggestion()) {

initSuggestionInputs로 자동완성 처리된 Input은 별도 EDITABLE 모델 로직이 동작하므로 제외.


3. onAfterRendering에서 이벤트 바인딩

SAP UI5는 렌더링마다 DOM이 새로 생성되기 때문에, onAfterRendering 안에서 이벤트를 바인딩해야 안전.

이벤트 네임스페이스 focus.readonly를 사용하면 off("focus.readonly")로 해당 이벤트만 정확히 제거 가능 → 중복 바인딩 방지.


4. this.blur() — 포커스 즉시 제거

$input.on("focus.readonly", function() {
    this.blur();
});

포커스가 잡히는 순간 blur() 호출 → 키보드 입력 원천 차단.


사용 방법

// onAfterRendering 또는 데이터 바인딩 직후
CommonSuggestions.initReadInput(this, "addressInput");

// 여러 개 동시 처리
CommonSuggestions.initReadInput(this, ["addressInput", "detailAddressInput"]);

방법 비교

방법 비활성화 UI 타이핑 차단 값 세팅 가능
editable="false" 회색 처리됨 O O
readonly 속성 X O 경우에 따라 다름
initReadInput X (정상 외관 유지) O O

팁: jQuery 이벤트 네임스페이스(.readonly)를 활용하면 off()로 특정 이벤트만 제거 가능 → 이벤트 중복 바인딩 버그 예방.

1. 현상

  • Grid 테이블에서 특정 조건(예: 상태가 '에러'인 행)에 따라 특정 행의 셀 배경색 스타일을 다르게 적용하고 싶을 때 사용.

2. 원인

  • SAPUI5 테이블의 각 행은 화면 갱신(스크롤, 정렬, 필터 등)이 일어날 때마다 DOM과 데이터 바인딩 상태가 변경됨. 따라서 단순 1회성 DOM 조작이 아니라 갱신 주기마다 데이터를 체크하여 스타일을 동적으로 입혀주어야 함.

3. 해결 코드

  • 테이블의 rowsUpdated 이벤트를 가로채서 화면이 갱신될 때마다 현재 바인딩된 실제 데이터를 조회하여 스타일 클래스를 동적으로 초기화 및 재할당함.
setRowsStyle : function(oTable, modelId, fn){
    if(!oTable._chRowStyleUpdate){
        oTable.attachEvent("rowsUpdated", function(){
            var rows = oTable.getRows();
            rows.forEach(function(row){
                var context = row.getBindingContext(modelId);
                var rowData = context ? context.getObject() : null;
                var $cells = $(row.getDomRef()).find('.sapUiTableCellInner');

                // 기존에 적용되었던 커스텀 스타일 클래스(accGridRow로 시작하는 클래스) 일괄 제거
                $cells.removeClass(function(index, className){
                    return (className.match(/\baccGridRow\w*\b/g) || []).join(' ');
                });

                // 콜백 함수를 실행하여 조건에 맞는 클래스명을 획득 및 적용
                var classNm = rowData ? fn(rowData) : "";
                if(classNm){
                    $cells.addClass(classNm);
                }
            });
        });
        oTable._chRowStyleUpdate = true;
    }
}

4. 핵심 로직 요약

  • 이벤트 기반 갱신: rowsUpdated 시점에 매번 스타일을 부여하여 정렬, 필터, 화면 이동 등 테이블 갱신 상황과 완벽 동기화.
  • 커스텀 스타일 초기화: 스타일을 새로 입히기 전 정규식(\baccGridRow\w*\b)을 이용해 이전 스타일을 지워줌으로써 스타일 중복 방지.

5. 사용 방법

  • 테이블을 초기화하거나 데이터를 로드하는 컨트롤러 시점에 호출.
// controller.js 예시
onInit: function() {
    var oTable = this.byId("myTable");

    // 특정 상태값(STATUS)에 따라 행 스타일 분기 처리 예시
    CommonCustomCore.setRowsStyle(oTable, "myModel", function(rowData) {
        if (rowData.STATUS === "E") {
            return "accGridRowRed";  // CSS 파일에 정의된 빨간색 배경 클래스 반환
        } else if (rowData.STATUS === "S") {
            return "accGridRowBlue"; // 파란색 배경 클래스 반환
        }
        return ""; // 조건이 없을 경우 빈 값 반환
    });
}

6. CSS 클래스 작성 예시 (style.css)

  • 네이밍 규칙: JS에서 기존 스타일을 일괄 제거할 때 accGridRow로 시작하는 클래스를 찾으므로, 모든 행 스타일 클래스는 반드시 accGridRow 접두사로 시작해야 함.
/* style.css 예시 */

/************************ 그리드 행 관련 class *****************/

/* 1. 조건 A 적용 시 (빨간색 굵게) */
.accGridRowRed,
.accGridRowRed .sapMText {
    color: #FF0318 !important;
    font-weight: bold !important;
}

/* 2. 조건 B 적용 시 (파란색 굵게) */
.accGridRowBlue,
.accGridRowBlue .sapMText {
    color: #0000FF !important;
    font-weight: bold !important;
}

1. 현상

  • setSelectedKeysetValue 등 코드로 값을 강제로 지정할 때, SAPUI5 내부 change 이벤트가 타지 않아 필수값 표시 스타일이 실시간으로 동기화되지 않는 문제 발생.

2. 원인

  • SAPUI5 엔진 특성상 사용자가 직접 타이핑/클릭해서 값을 바꿀 때만 이벤트를 타며, 코드로 직접 값을 세팅하는 API 호출 시에는 변경 이벤트를 트리거하지 않기 때문.

3. 해결 코드

  • 주요 입력 컨트롤(ComboBox, Select, Input, DatePicker)의 값 변경 및 편집 상태 메서드를 오버라이드하여, 값 지정 직후 스타일 체크 함수가 강제 동작하도록 구성.
overrideRequiredStyle : function(){
    function ch(obj, k){
        // 편집 불가능 상태일 경우 필수값 스타일 제거
        if (obj.getEditable && !obj.getEditable()) {
            obj.removeStyleClass('customRequired');
            return;
        }
        // 필수값이고 편집 가능한 상태일 때 체크
        if (obj.getRequired && obj.getRequired() && obj.getEditable && obj.getEditable()) {
            if (!!k) {
                // 값이 있으면 스타일 클래스 제거
                obj.removeStyleClass('customRequired');
            } else {
                // 값이 비어있으면 필수 입력 스타일 클래스 추가
                obj.addStyleClass('customRequired');
            }
        }
    }

    function overrideAndChange(ctrl, methodNm, valFn) {
        var _original = ctrl.prototype[methodNm];
        ctrl.prototype[methodNm] = function() {
            var result = _original.apply(this, arguments);
            ch(this, valFn.call(this)); // 변경 이후 스타일 체크 강제 수행
            return result;
        };
    }

    // ComboBox
    overrideAndChange(ComboBox, "setSelectedKey", ComboBox.prototype.getSelectedKey);
    overrideAndChange(ComboBox, "setValue", ComboBox.prototype.getValue);
    overrideAndChange(ComboBox, "setEditable", ComboBox.prototype.getValue);

    // Select
    overrideAndChange(Select, "setSelectedKey", Select.prototype.getSelectedKey);
    overrideAndChange(Select, "setEditable", Select.prototype.getSelectedKey);

    // Input
    overrideAndChange(Input, "setValue", Input.prototype.getValue);
    overrideAndChange(Input, "setEditable", Input.prototype.getValue);

    // DatePicker
    overrideAndChange(DatePicker, "setValue", DatePicker.prototype.getValue);
    overrideAndChange(DatePicker, "setEditable", DatePicker.prototype.getValue);
}

4. 핵심 로직 요약

  • 상태 감지(ch): 객체의 편집 가능 여부(editable)와 필수값 지정 여부(required)를 조회한 후, 값의 유무(!!k)에 따라 customRequired 클래스를 붙이거나 뗌.
  • 프로토타입 래핑: 각 컨트롤의 주요 데이터 반영 메서드를 인터셉트하여 기존 비즈니스 로직에 영향 없이 보정 로직을 공통 수행하도록 설계.

5번에 들어갈 사용 방법 항목을 간단하게 요약해서 구성했어.


5. 사용 방법

  • 호출 시점: 앱이 초기화되는 시점인 Component.jsinit 함수 내에서 최초 1회만 호출하여 프로젝트 전체에 적용.
// Component.js 예시
sap.ui.define([
    "sap/ui/core/UIComponent",
    "ddi/eaccounting/controller/CommonUtility/CommonCustomCore" // 공통 커스텀 유틸 파일 임포트
], function(UIComponent, CommonCustomCore) {
    "use strict";

    return UIComponent.extend("ddi.eaccounting.Component", {
        init: function() {
            // 부모 init 실행
            UIComponent.prototype.init.apply(this, arguments);

            // 필수값 스타일 오버라이드 함수 실행 (프로젝트 전역 적용)
            CommonCustomCore.overrideRequiredStyle();
        }
    });
});

1. 현상

  • CSS(!important)로 Grid 테이블 행 높이 및 테이블 최대 높이(max-height)를 조정하면 스크롤바가 끝까지 안 내려가거나 버벅임.

2. 원인

  • SAPUI5 내부 렌더러가 가상 스크롤 높이(.sapUiTableVSbContent)를 계산할 때, 커스텀 CSS 높이가 아닌 하드코딩된 행 높이 기준값(_getBaseRowHeight())을 사용하여 높이 불일치 발생.

3. 해결 코드

  • Table.prototype.onAfterRendering을 전역 오버라이드하여 렌더링 시점에 스크롤바 높이를 강제 재계산 및 고정.
overrideTableScrollHeight: function () {
    var _originalOnAfterRendering = Table.prototype.onAfterRendering;
    Table.prototype.onAfterRendering = function () {
        if (_originalOnAfterRendering) {
            _originalOnAfterRendering.apply(this, arguments);
        }
        if (!this._scrollHeightFixAttached) {
            this._scrollHeightFixAttached = true;
            var oTable = this;

            var _fixScrollContentHeight = function () {
                var oDomRef = oTable.getDomRef();
                if (!oDomRef) return;

                var visibleRowCount = oTable.getVisibleRowCount();
                var oBinding = oTable.getBinding("rows");
                var totalRowCount = oBinding ? oBinding.getLength() : 0;
                if (totalRowCount <= visibleRowCount) return;

                var vSb = oDomRef.querySelector(".sapUiTableVSb");
                var vSbContent = oDomRef.querySelector(".sapUiTableVSbContent");
                if (!vSbContent || !vSb) return;

                var baseRowHeight = oTable._getBaseRowHeight();
                var cssMaxHeight = parseFloat(window.getComputedStyle(vSb).maxHeight);
                if (isNaN(cssMaxHeight)) {
                    cssMaxHeight = visibleRowCount * baseRowHeight;
                }

                var correctHeight = Math.ceil(cssMaxHeight + (totalRowCount - visibleRowCount) * baseRowHeight);
                var currentHeight = parseInt(vSbContent.style.height, 10);

                if (currentHeight !== correctHeight) {
                    vSbContent.style.setProperty('height', correctHeight + 'px', 'important');
                }
            };

            var _setupObserver = function () {
                var oDomRef = oTable.getDomRef();
                if (!oDomRef) return;

                var vSbContent = oDomRef.querySelector(".sapUiTableVSbContent");
                if (!vSbContent) return;

                var observer = new MutationObserver(function () {
                    _fixScrollContentHeight();
                });
                observer.observe(vSbContent, { attributes: true, attributeFilter: ["style"] });
                oTable._scrollHeightObserver = observer;
            };

            _setupObserver();

            this.attachEvent("_rowsUpdated", function () {
                setTimeout(function () {
                    if (oTable._scrollHeightObserver) {
                        oTable._scrollHeightObserver.disconnect();
                    }
                    _setupObserver();
                    _fixScrollContentHeight();
                }, 50);
            });
        }
    };
}

4. 핵심 로직 요약

  • 실제 CSS 값 반영: window.getComputedStyle로 최종 적용된 maxHeight를 동적으로 수집.
  • 강제 덮어쓰기 방지: MutationObserver를 사용하여 SAPUI5 엔진이 스크롤 시 스크롤바 높이를 되돌리는 현상을 차단.
  • 이벤트 동기화: _rowsUpdated 이벤트를 트리거로 삼아 데이터가 변경될 때마다 보정 로직 재실행.

5. 사용 방법

  • 호출 시점: 앱이 구동되는 최초 시점인 Component.jsinit 함수 내에서 호출하여 프로젝트 전역에 일괄 적용.
// Component.js 예시
sap.ui.define([
    "sap/ui/core/UIComponent",
    "ddi/eaccounting/controller/CommonUtility/CommonCustomCore" // 공통 커스텀 유틸 파일 임포트
], function(UIComponent, CommonCustomCore) {
    "use strict";

    return UIComponent.extend("ddi.eaccounting.Component", {
        init: function() {
            // 부모 init 실행
            UIComponent.prototype.init.apply(this, arguments);

            // 테이블 스크롤 높이 보정 함수 실행 (프로젝트 전역 적용)
            CommonCustomCore.overrideTableScrollHeight();
        }
    });
});