UI5

Object Page 섹션 30초 만에 구성 #shorts #SAP #RAP

▶ YouTube에서 보기

1. Object Page 레이아웃과 섹션의 역할

SAP Fiori Elements의 Object Page는 단일 비즈니스 엔티티(예: 판매주문 한 건, 거래처 한 곳)의 상세 정보를 표시하기 위한 표준 플로어플랜입니다. List Report에서 행을 클릭했을 때 펼쳐지는 그 페이지를 떠올리면 됩니다. Object Page는 크게 세 영역으로 나뉩니다.

  • Header 영역: 엔티티 식별 정보(주문번호, 고객명), KPI, 마이크로차트, 상태(Object Marker) 등을 표시합니다.
  • Anchor Bar: 각 섹션으로 빠르게 점프할 수 있는 내비게이션 바입니다. 섹션을 정의하면 자동으로 채워집니다.
  • Content 영역(Sections): 본문에 해당하는 부분이며, 여러 개의 섹션과 서브섹션으로 구성됩니다.

섹션(Section)은 의미 있는 정보 묶음을 표현하는 컨테이너입니다. 예를 들어 SalesOrder Object Page에서는 "일반정보", "결제조건", "배송지 정보", "주문 항목(Items)"이 각각 하나의 섹션이 됩니다. 서브섹션(Subsection)은 한 섹션 내부에서 다시 정보를 그룹핑할 때 사용합니다. "결제조건" 섹션 아래에 "통화/금액"과 "지불방식" 두 개의 서브섹션을 두는 식입니다.

이 구조는 정보 밀도가 높은 화면에서 사용자의 인지부하를 줄여줍니다. 한 화면에 100개의 필드를 늘어놓으면 사용자는 어디를 봐야 할지 알 수 없지만, 8개의 섹션으로 묶어두면 스캔과 점프가 가능해집니다.

2. CDS 어노테이션으로 FieldGroup 정의하기

섹션을 구성하기 위한 가장 작은 단위는 FieldGroup입니다. FieldGroup은 "함께 보여줄 필드들의 묶음"을 정의하는 어노테이션이며, 이후 Facet에서 이 FieldGroup을 참조하여 섹션으로 렌더링합니다.

아래는 SalesOrder 엔티티에서 일반 정보와 결제 조건을 각각 FieldGroup으로 정의한 예제입니다. ABAP CDS 또는 RAP 기반 프로젝션 뷰에서 사용하는 어노테이션 문법을 따릅니다.

@Metadata.layer: #CORE
annotate view ZC_SalesOrder with
{
  @UI.fieldGroup: [ { qualifier: 'GeneralInfo',
                      position: 10,
                      label: '일반 정보' } ]
  SalesOrderId;

  @UI.fieldGroup: [ { qualifier: 'GeneralInfo', position: 20 } ]
  OrderDate;

  @UI.fieldGroup: [ { qualifier: 'GeneralInfo', position: 30 } ]
  CustomerId;

  @UI.fieldGroup: [ { qualifier: 'PaymentTerms', position: 10 } ]
  CurrencyCode;

  @UI.fieldGroup: [ { qualifier: 'PaymentTerms', position: 20 } ]
  TotalAmount;

  @UI.fieldGroup: [ { qualifier: 'PaymentTerms', position: 30 } ]
  PaymentMethod;
}

qualifier 값은 FieldGroup의 고유 식별자이며, 이후 ReferenceFacet에서 이 이름으로 참조합니다. position은 필드의 정렬 순서를 결정하며 10, 20, 30처럼 10단위로 띄워두는 것이 일반적인 컨벤션입니다. 나중에 사이에 필드를 끼워 넣어야 할 때 15, 25 같은 값을 줄 수 있기 때문입니다.

3. ReferenceFacet으로 단일 섹션 구성

FieldGroup만 정의한다고 화면에 섹션이 나타나지는 않습니다. UI.Facets 배열에 ReferenceFacet 항목을 추가하여 "이 FieldGroup을 섹션으로 보여달라"고 선언해야 합니다.

@UI.facet: [
  {
    id:            'GeneralInfoFacet',
    type:          #FIELDGROUP_REFERENCE,
    label:         '일반 정보',
    targetQualifier: 'GeneralInfo',
    position:      10
  },
  {
    id:            'PaymentTermsFacet',
    type:          #FIELDGROUP_REFERENCE,
    label:         '결제 조건',
    targetQualifier: 'PaymentTerms',
    position:      20
  }
]
annotate view ZC_SalesOrder with {
  @UI.hidden: false
  SalesOrderId;
}

type에 #FIELDGROUP_REFERENCE를 지정하면 OData EDMX로 변환될 때 UI.ReferenceFacet으로 매핑됩니다. targetQualifier는 앞서 정의한 FieldGroup의 qualifier와 정확히 일치해야 합니다. id는 다른 Facet에서 부모로 참조할 때 사용되므로, 의미 있는 이름을 권장합니다.

이렇게 작성된 어노테이션은 Object Page에서 두 개의 독립된 섹션으로 렌더링되며, Anchor Bar에 자동으로 등록됩니다.

4. CollectionFacet으로 멀티 서브섹션 구성

업무 화면에서는 종종 "한 섹션 안에 여러 그룹"을 표현해야 합니다. 예를 들어 "거래처 정보"라는 상위 섹션 아래에 "기본 정보", "주소", "연락처" 세 개의 서브섹션을 두는 케이스입니다. 이때 CollectionFacet을 사용합니다.

@UI.facet: [
  {
    id:       'BusinessPartnerOverview',
    type:     #COLLECTION,
    label:    '거래처 정보',
    position: 10
  },
  {
    parentId:        'BusinessPartnerOverview',
    id:              'BPBasic',
    type:            #FIELDGROUP_REFERENCE,
    label:           '기본 정보',
    targetQualifier: 'BPBasic',
    position:        10
  },
  {
    parentId:        'BusinessPartnerOverview',
    id:              'BPAddress',
    type:            #FIELDGROUP_REFERENCE,
    label:           '주소',
    targetQualifier: 'BPAddress',
    position:        20
  },
  {
    parentId:        'BusinessPartnerOverview',
    id:              'BPContact',
    type:            #FIELDGROUP_REFERENCE,
    label:           '연락처',
    targetQualifier: 'BPContact',
    position:        30
  }
]

핵심은 parentId 속성입니다. 자식 ReferenceFacet의 parentId 값을 부모 CollectionFacet의 id와 일치시키면 계층 구조가 형성됩니다. 화면에서는 상위 섹션 헤더 하나 아래에 여러 서브섹션이 가로 또는 세로로 배치됩니다.

주의할 점은 Object Page가 일반적으로 2단계 계층까지만 시각적으로 구분된다는 것입니다. CollectionFacet 안에 또 CollectionFacet을 중첩하면 세 번째 레벨은 렌더링되지 않거나 평탄화될 수 있으니 설계를 단순하게 유지하는 것이 좋습니다.

5. 섹션 순서 제어와 중요도 설정

섹션 순서는 facet 배열의 position 값으로 결정됩니다. 숫자가 작을수록 위쪽 또는 왼쪽에 배치됩니다. position을 명시하지 않으면 어노테이션이 선언된 순서대로 처리되지만, 향후 유지보수를 고려하면 항상 명시적으로 부여하는 편이 안전합니다.

필드 단위의 중요도는 UI.importance 어노테이션으로 제어합니다. 화면 폭이 좁아지면(태블릿/모바일) 낮은 중요도 필드는 자동으로 숨겨지거나 더보기 영역으로 이동합니다.

annotate view ZC_PurchaseOrder with
{
  @UI.fieldGroup: [ { qualifier: 'POHeader', position: 10,
                      importance: #HIGH } ]
  PurchaseOrderId;

  @UI.fieldGroup: [ { qualifier: 'POHeader', position: 20,
                      importance: #HIGH } ]
  SupplierId;

  @UI.fieldGroup: [ { qualifier: 'POHeader', position: 30,
                      importance: #MEDIUM } ]
  CreatedBy;

  @UI.fieldGroup: [ { qualifier: 'POHeader', position: 40,
                      importance: #LOW } ]
  LastChangedAt;
}

importance는 #HIGH, #MEDIUM, #LOW 세 가지 값을 가집니다. 좁은 뷰포트에서 우선 표시할 필드는 #HIGH로, 감사 정보처럼 보조적인 필드는 #LOW로 두는 것이 일반적인 패턴입니다.

6. 조건부 섹션 표시

모든 섹션이 항상 보여야 하는 것은 아닙니다. 예를 들어 "취소된 주문"에 대해서만 "취소 사유" 섹션을 보여주거나, 특정 권한을 가진 사용자에게만 "원가" 섹션을 노출해야 할 수 있습니다. 이때 UI.hidden 어노테이션을 사용합니다.

@UI.facet: [
  {
    id:              'CancellationFacet',
    type:            #FIELDGROUP_REFERENCE,
    label:           '취소 정보',
    targetQualifier: 'Cancellation',
    position:        90,
    hidden:          'IsCancelled'
  }
]
annotate view ZC_SalesOrder with
{
  @UI.hidden: true
  IsCancelled;
}

hidden 속성에 boolean 리터럴(true/false) 대신 엔티티의 속성 경로를 넘기면 동적 평가가 이뤄집니다. 위 예에서는 IsCancelled가 true인 레코드에서만 해당 섹션이 사라집니다. 반대 로직이 필요하면 CDS 측에서 파생 필드를 만들어 두거나, 표시 조건용 가상 속성을 추가하는 패턴이 자주 쓰입니다.

완전 정적으로 감추려면 hidden: true를 그대로 주면 됩니다. 권한 기반 숨김은 보통 백엔드 Behavior Definition에서 권한 체크 후 가상 속성을 채워 내려주는 방식을 권장합니다.

7. Custom Section 추가

표준 어노테이션만으로 표현하기 어려운 UI(예: 외부 시스템에서 가져온 그래프, 지도 위 배송 경로 시각화)가 필요할 때는 Custom Section을 추가합니다. 이는 SAPUI5 Fragment 파일과 manifest.json의 확장 설정으로 구현합니다.

{
  "sap.ui5": {
    "routing": {
      "targets": {
        "SalesOrderObjectPage": {
          "options": {
            "settings": {
              "content": {
                "body": {
                  "sections": {
                    "DeliveryMapSection": {
                      "template": "zsalesorder.ext.fragment.DeliveryMap",
                      "position": {
                        "placement": "After",
                        "anchor": "GeneralInfoFacet"
                      },
                      "title": "배송 경로",
                      "subTitle": "실시간 위치 기반"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

placement에는 After / Before, anchor에는 기존 Facet의 id를 지정합니다. template은 Fragment 경로이며, 해당 Fragment 안에서 UI5 컨트롤(예: sap.ui.vbm 기반 지도 컨트롤, 사용자 정의 차트)을 자유롭게 사용할 수 있습니다.

<core:FragmentDefinition
    xmlns="sap.m"
    xmlns:core="sap.ui.core">
    <VBox>
        <Title text="{= \${DeliveryStatus} === 'IN_TRANSIT' ? '운송 중' : '대기' }" />
        <Text text="현재 위치: {CurrentLocation}" />
    </VBox>
</core:FragmentDefinition>

Custom Section은 강력하지만 남용하면 Fiori Elements가 제공하는 자동 반응형/접근성 혜택을 잃을 수 있습니다. 표준 어노테이션으로 표현 가능한 경우에는 표준을 우선 사용하고, 정말 필요한 곳에만 Custom을 도입하는 균형이 중요합니다.

8. 실전 검증과 흔한 실수 패턴

구성을 마쳤다면 BAS(Business Application Studio) 또는 VS Code의 Fiori Tools에서 Preview를 실행해 Anchor Bar에 섹션이 의도한 순서대로 나타나는지, 각 섹션에 필드가 올바르게 묶였는지 확인합니다. /sap/opu/odata/... 의 $metadata를 직접 열어 UI.Facets 컬렉션이 EDMX로 잘 변환됐는지 점검하는 것도 권장됩니다.

현장에서 가장 자주 마주치는 실수 세 가지는 다음과 같습니다.

  • qualifier 오타: targetQualifier에 적힌 이름이 실제 FieldGroup qualifier와 한 글자라도 다르면 섹션이 비어 보이거나 사라집니다. 대소문자도 구분되므로 그대로 복사·붙여넣기로 일치시키는 습관이 안전합니다.
  • parentId 누락: CollectionFacet 자식인데 parentId를 빠뜨리면 자식 Facet이 최상위 섹션으로 따로 떨어져 나옵니다. Anchor Bar에 의도치 않은 섹션이 생겼다면 가장 먼저 의심할 항목입니다.
  • position 충돌: 동일한 position 값이 여러 Facet에 부여되면 렌더 순서가 비결정적이 됩니다. 빌드는 성공하지만 환경에 따라 순서가 뒤바뀌어 보일 수 있으니, 항상 유일한 position을 부여합니다.

이외에도 hidden 속성에 존재하지 않는 속성 경로를 넘겨 런타임 콘솔에 경고가 찍히는 경우, Custom Section의 Fragment 경로 오타로 빈 영역이 표시되는 경우가 흔합니다. 브라우저 개발자 도구의 콘솔과 Network 탭의 $metadata 응답을 함께 보면 대부분의 문제는 빠르게 추적됩니다.

섹션 구성은 Object Page의 가독성과 사용성을 결정짓는 핵심 작업입니다. FieldGroup → ReferenceFacet/CollectionFacet → 순서·중요도·조건부 표시 → 필요 시 Custom Section 순으로 단계적으로 쌓아 올리는 흐름을 익히면, 어떤 비즈니스 엔티티든 일관된 품질의 상세 화면을 빠르게 만들 수 있습니다.

댓글 0

아직 댓글이 없습니다.