RAP

Global Action 없이 버튼 — RAP 실패 3가지 #shorts #SAP #RAP

버튼이 화면에 아예 나타나지 않는 이유 — 흔한 실패 시나리오

RAP(ABAP RESTful Application Programming Model)으로 List Report를 만들다 보면 "테이블 위쪽 툴바에 커스텀 버튼 하나만 추가하고 싶은데 아무리 해도 안 보인다"는 상황을 자주 만나게 됩니다. 이 글은 SAP BTP ABAP Environment 및 SAP S/4HANA 2022 이상(ABAP Platform 2022, RAP strict mode 2 기준) 환경을 전제로, 실패 원인을 짚고 Global Action(Static/Unbound Action)으로 올바르게 구현하는 전 과정을 다룹니다.

전형적인 실패 패턴은 세 가지입니다.

  • 일반 Instance Action만 선언한 경우 — 버튼은 생기지만 행을 선택하기 전까지 회색 비활성 상태이며, "선택 없이 항상 누를 수 있는 버튼"이라는 요구사항을 만족하지 못합니다.
  • Behavior Definition에만 Action을 선언하고 CDS 어노테이션을 빠뜨린 경우 — OData 메타데이터에는 Action이 존재하지만 Fiori Elements가 버튼을 그릴 근거(UI 어노테이션)가 없어 화면에 아무것도 나타나지 않습니다.
  • 프론트엔드(XML View, Extension)에서 직접 버튼을 만들려는 경우 — Fiori Elements 기반 List Report는 어노테이션 주도로 렌더링되므로, RAP 백엔드와 연결되지 않은 버튼은 유지보수 부담만 커집니다.

결론부터 말하면, 선택 없이 항상 활성화되는 툴바 버튼은 Behavior Definition의 static action + CDS의 #FOR_ACTION 어노테이션 조합으로 구현하는 것이 일반적으로 권장되는 방식입니다.

Instance Action과 Static(Global) Action — 무엇이 언제 활성화되는가

이 주제를 이해하려면 RAP Action의 두 종류를 구분해야 합니다. 비유하자면 Instance Action은 "특정 문서에 찍는 도장"이고, Static Action은 "부서 전체 회람에 거는 공지 버튼"입니다.

구분Instance ActionStatic(Global) Action
대상선택된 엔티티 인스턴스인스턴스와 무관 (엔티티 집합 전체)
버튼 활성 조건행 선택 시 활성화항상 활성화
BDEF 문법action release;static action massApprove;
OData 노출인스턴스 바인딩 Action컬렉션 레벨 Action (Fiori에서 Global 동작)
대표 용례단건 승인, 상태 전환일괄 승인, 배치 잡 트리거, Export

동작 원리를 보면, Fiori Elements List Report는 OData 메타데이터의 Action 바인딩 정보와 @UI 어노테이션을 읽어 툴바를 구성합니다. Instance Action은 "이 Action은 특정 키가 필요하다"고 선언되어 있으므로 프레임워크가 자동으로 선택 여부와 버튼 활성화를 연동합니다. 반면 static action은 키가 필요 없다고 선언되므로 항상 클릭 가능한 상태로 렌더링됩니다. 초보자가 겪는 "버튼이 계속 회색"인 문제의 8할은 이 차이를 몰라서 발생합니다.

CDS 어노테이션으로 List Report 툴바에 버튼 노출하기

예제 시나리오는 구매 오더(Purchase Order) 일괄 승인입니다. Projection View(Consumption View) 또는 Metadata Extension에 #FOR_ACTION 타입의 lineItem 어노테이션을 추가하면 테이블 툴바에 버튼이 생성됩니다.

@Metadata.layer: #CORE
annotate view ZC_PurchOrd with
{
  @UI.lineItem: [
    { position: 10, importance: #HIGH, label: '구매 오더' },
    { type: #FOR_ACTION, dataAction: 'massApprove',
      label: '미결 오더 일괄 승인', position: 1 },
    { type: #FOR_ACTION, dataAction: 'release',
      label: '승인 요청', position: 2 }
  ]
  PurchaseOrder;

  @UI.lineItem: [{ position: 20 }]
  Supplier;

  @UI.lineItem: [{ position: 30, criticality: 'StatusCriticality' }]
  OverallStatus;
}

핵심 포인트는 다음과 같습니다.

  • dataAction에는 Behavior Definition에서 선언할 Action 이름을 대소문자까지 정확히 적어야 합니다. 오타가 나도 활성화 에러가 발생하지 않고 조용히 버튼만 사라지므로 주의가 필요합니다.
  • type: #FOR_ACTION이 붙은 lineItem은 컬럼이 아니라 테이블 툴바 버튼으로 렌더링됩니다.
  • 같은 어노테이션이라도 연결된 Action이 static이면 항상 활성, instance면 선택 연동 — 버튼의 성격은 어노테이션이 아니라 BDEF 선언이 결정합니다.

Behavior Definition에 Static Action 선언하기

이제 백엔드 계약을 정의합니다. Root View ZR_PurchOrd에 대한 managed 시나리오 Behavior Definition입니다.

managed implementation in class zbp_r_purchord unique;
strict ( 2 );

define behavior for ZR_PurchOrd alias PurchaseOrder
persistent table zpurchord
lock master
authorization master ( instance )
etag master LastChangedAt
{
  create;
  update;
  delete;

  " Global Action: 선택 없이 미결 오더 전체를 승인
  static action massApprove;

  " 비교용 Instance Action: 선택한 오더만 승인 요청
  action ( features : instance ) release result [1] $self;

  field ( readonly ) PurchaseOrder, OverallStatus, LastChangedAt;
  mapping for zpurchord corresponding;
}

Projection이 있는 구조라면 Behavior Projection에도 반드시 Action을 재노출해야 합니다. 이 단계를 빠뜨리면 서비스 메타데이터에 Action이 나오지 않아 버튼도 사라집니다.

projection;
strict ( 2 );

define behavior for ZC_PurchOrd alias PurchaseOrder
{
  use create;
  use update;
  use delete;

  use action massApprove;
  use action release;
}

파라미터가 필요한 경우(예: 승인 사유 입력 팝업) static action massApprove parameter ZD_ApproveParam;처럼 추상 CDS 엔티티를 파라미터로 지정하면, Fiori Elements가 자동으로 입력 다이얼로그를 생성해 줍니다.

Behavior Pool 클래스에서 Action 로직 구현하기

선언만 하고 활성화하면 "method massapprove is not implemented" 류의 경고가 나옵니다. Behavior Pool(zbp_r_purchord)의 Local Handler에 구현을 추가합니다. static action의 keys에는 인스턴스 키가 없고 %cid만 전달된다는 점이 Instance Action과 다릅니다.

CLASS lhc_purchaseorder DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    CONSTANTS: c_open     TYPE zde_po_status VALUE 'O',
               c_approved TYPE zde_po_status VALUE 'A'.
    METHODS massapprove FOR MODIFY
      IMPORTING keys FOR ACTION purchaseorder~massapprove.
    METHODS release FOR MODIFY
      IMPORTING keys FOR ACTION purchaseorder~release RESULT result.
ENDCLASS.

CLASS lhc_purchaseorder IMPLEMENTATION.
  METHOD massapprove.
    " 1) 처리 대상 조회: 미결 상태의 구매 오더 전체
    SELECT purchaseorder
      FROM zr_purchord
      WHERE overallstatus = @c_open
      INTO TABLE @DATA(lt_open_orders).

    IF lt_open_orders IS INITIAL.
      APPEND VALUE #( %msg = new_message_with_text(
                        severity = if_abap_behv_message=>severity-information
                        text     = '승인 대상 미결 오더가 없습니다.' ) )
             TO reported-purchaseorder.
      RETURN.
    ENDIF.

    " 2) EML로 상태 일괄 변경 (LOCAL MODE: 자체 검증 재실행 방지)
    MODIFY ENTITIES OF zr_purchord IN LOCAL MODE
      ENTITY purchaseorder
        UPDATE FIELDS ( overallstatus )
        WITH VALUE #( FOR ls_po IN lt_open_orders
                      ( purchaseorder = ls_po-purchaseorder
                        overallstatus = c_approved ) )
      FAILED   DATA(lt_failed)
      REPORTED DATA(lt_reported).

    " 3) 실패 전파 + 성공 메시지
    failed-purchaseorder   = CORRESPONDING #( lt_failed-purchaseorder ).
    reported-purchaseorder = CORRESPONDING #( lt_reported-purchaseorder ).

    APPEND VALUE #( %msg = new_message_with_text(
                      severity = if_abap_behv_message=>severity-success
                      text     = |{ lines( lt_open_orders ) }건의 오더를 승인했습니다.| ) )
           TO reported-purchaseorder.
  ENDMETHOD.

  METHOD release.
    " Instance Action: 선택된 키만 처리
    MODIFY ENTITIES OF zr_purchord IN LOCAL MODE
      ENTITY purchaseorder
        UPDATE FIELDS ( overallstatus )
        WITH VALUE #( FOR key IN keys
                      ( %tky = key-%tky  overallstatus = c_approved ) ).

    READ ENTITIES OF zr_purchord IN LOCAL MODE
      ENTITY purchaseorder ALL FIELDS WITH CORRESPONDING #( keys )
      RESULT DATA(lt_po).
    result = VALUE #( FOR ls IN lt_po ( %tky = ls-%tky %param = ls ) ).
  ENDMETHOD.
ENDCLASS.

실무 관점 체크리스트: IN LOCAL MODE로 권한 재검사와 feature control 재실행을 건너뛰되, 그만큼 Action 자체에 대한 권한 검증(예: 승인 권한 오브젝트 체크)을 별도로 넣어야 합니다. 또한 대량 데이터라면 SELECT에 UP TO n ROWS와 패키지 처리로 LUW 크기를 통제하는 것이 안전합니다.

결과·메시지 바인딩 — failed와 reported가 사용자 경험을 좌우한다

Global Action은 화면에 즉각적인 행 변경이 보이지 않기 때문에, 사용자는 메시지로만 결과를 인지합니다. 그래서 reported 구조 채우기가 특히 중요합니다.

  • reported-<alias>%msg만 채우면(키 없이) Fiori가 메시지 팝업/토스트로 표시합니다.
  • 특정 오더에 대한 에러라면 %tky를 함께 채워 해당 행과 메시지를 연결할 수 있습니다.
  • 에러 상황에서 failed를 채우지 않으면 트랜잭션이 정상 커밋된 것으로 간주되어, 사용자에게 실패가 전달되지 않는 사고가 납니다.

메시지 클래스를 쓰는 경우 new_message( id = 'ZPO_MSG' number = '003' severity = ... v1 = lv_count ) 형태로 T100 기반 메시지를 사용하는 것이 다국어 관점에서 일반적으로 권장됩니다.

Fiori Preview 검증과 트러블슈팅 FAQ

Service Binding(OData V4 – UI)에서 엔티티를 선택하고 Preview를 실행하면, 테이블 툴바 왼쪽에 "미결 오더 일괄 승인" 버튼이 행 선택 여부와 무관하게 활성 상태로 보여야 성공입니다. 자주 나오는 질문을 정리합니다.

  • Q1. 버튼이 아예 안 보입니다. — ① dataAction 이름과 BDEF Action 이름 불일치, ② Behavior Projection에 use action 누락, ③ 메타데이터 캐시가 원인의 대부분입니다. Metadata Extension·BDEF·Service Binding을 모두 재활성화하고 브라우저 강력 새로고침(캐시 삭제) 후 확인하세요.
  • Q2. 버튼이 회색으로 비활성입니다. — static이 아닌 instance action에 연결된 경우입니다. BDEF에 static 키워드가 있는지, feature control(features : instance)이 의도치 않게 걸려 있지 않은지 확인하세요.
  • Q3. 클릭하면 500/422 에러가 납니다. — Handler 미구현이거나, draft 사용 시나리오에서 활성 인스턴스 전제 로직이 draft 키와 충돌하는 경우가 많습니다. ADT의 Feed Reader 또는 런타임 에러 로그(RABAX)를 먼저 확인하는 것이 빠릅니다.
  • Q4. 처리는 되는데 목록이 갱신되지 않습니다. — Global Action은 자동으로 목록을 새로 읽지 않을 수 있습니다. OData V4 기준 Side Effects 어노테이션으로 대상 엔티티 갱신을 지정하거나, 사용자가 Go/새로고침을 누르도록 안내 메시지를 넣는 방식이 현실적인 대안입니다.

실전 활용 패턴과 더 깊이 보기 위한 문서

Global Action은 다음 패턴에서 특히 유용합니다.

  • 일괄 처리: 미결 송장 일괄 생성, 기간 마감 오더 일괄 취소 — 이 글의 massApprove 패턴 그대로 확장
  • 백그라운드 잡 트리거: Action 내부에서 bgPF 또는 Application Job을 큐잉하고 즉시 접수 메시지만 반환
  • 파라미터 다이얼로그: 추상 엔티티 파라미터로 마감 연월 입력을 받아 정산 실행
  • 탐색형 Action: result로 생성된 문서를 반환해 Object Page로 자동 이동

다음 단계로는 factory action(인스턴스 복제 기반 생성), determine action, Side Effects, 그리고 draft 활성 시 Action 동작 차이를 학습하면 RAP UI 제어의 큰 그림이 완성됩니다. 아래 문서로 개념을 검증하며 확장해 보시길 권합니다.

댓글 0

아직 댓글이 없습니다.