카드 탐색

대부분의 카드 기반 부가기능은 부가기능 인터페이스의 여러 '페이지'를 나타내는 여러 카드를 사용하여 빌드됩니다. 효과적인 사용자 환경을 제공하려면 부가기능에서 카드 간에 간단하고 자연스러운 탐색을 사용해야 합니다.

원래 Gmail 부가기능에서는 단일 카드 스택으로 카드를 푸시 및 팝하여 UI의 여러 카드 간에 전환을 처리하며, 스택의 맨 위 카드는 Gmail에 표시됩니다.

홈페이지 카드 탐색

Google Workspace 부가기능에 홈페이지 및 문맥 외 카드가 도입되었습니다. 문맥 카드와 문맥 외 카드를 수용하기 위해 Google Workspace 부가기능에는 각각의 내부 카드 스택이 있습니다. 호스트에서 부가기능이 열리면 해당 homepageTrigger가 실행되어 스택의 첫 번째 홈페이지 카드 (아래 다이어그램의 진한 파란색 '홈페이지' 카드)를 만듭니다. homepageTrigger가 정의되지 않으면 기본 카드가 생성되고 표시되며 문맥 외 스택에 푸시됩니다. 이 첫 번째 카드는 루트 카드입니다.

부가기능은 문맥 외 카드를 추가로 만들고 사용자가 부가기능을 탐색할 때 스택 (다이어그램의 파란색 '푸시된 카드')에 푸시할 수 있습니다. 부가기능 UI는 스택의 맨 위 카드를 표시하므로 스택에 새 카드를 푸시하면 디스플레이가 변경되고 스택에서 카드를 팝하면 디스플레이가 이전 카드로 돌아갑니다.

부가기능에 정의된 문맥 트리거가 있는 경우 사용자가 해당 컨텍스트를 입력하면 트리거가 실행됩니다. 트리거 함수가 문맥 카드를 빌드하지만 UI 디스플레이는 새 카드의 DisplayStyle를 기반으로 업데이트됩니다.

  • DisplayStyleREPLACE (기본값)인 경우 문맥 카드 (다이어그램의 어두운 주황색 '문맥' 카드)가 현재 표시된 카드를 대체합니다. 이렇게 하면 컨텍스트가 없는 카드 스택 위에 새로운 컨텍스트 카드 스택이 시작되며 이 컨텍스트 카드는 컨텍스트 스택의 루트 카드입니다.
  • DisplayStylePEEK인 경우 UI는 대신 부가기능 사이드바 하단에 표시되어 현재 카드를 오버레이하는 엿보기 헤더를 만듭니다. 미리보기 헤더는 새 카드의 제목을 표시하고 사용자가 새 카드를 볼지 여부를 결정할 수 있는 사용자 버튼 컨트롤을 제공합니다. 사용자가 보기 버튼을 클릭하면 카드가 현재 카드를 대체합니다 (위에서 REPLACE로 설명).

문맥 카드를 추가로 만들고 스택에 푸시할 수 있습니다 (다이어그램의 노란색 '푸시된 카드'). 카드 스택을 업데이트하면 부가기능 UI가 변경되어 최상위 카드가 표시됩니다. 사용자가 컨텍스트를 벗어나면 비즈니스 카드 스택의 문맥 카드가 삭제되고 디스플레이가 최상단의 비문맥 카드 또는 홈페이지로 업데이트됩니다.

사용자가 부가기능에서 문맥 트리거를 정의하지 않은 컨텍스트를 입력하면 새 카드가 생성되지 않고 현재 카드가 계속 표시됩니다.

아래에 설명된 Navigation 작업은 동일한 컨텍스트의 카드에만 작동합니다. 예를 들어 상황별 카드 내의 popToRoot()는 다른 모든 상황별 카드만 팝하고 홈페이지 카드에는 영향을 미치지 않습니다.

반면 버튼은 사용자가 언제든지 문맥 카드에서 문맥 외 카드로 이동할 수 있도록 제공됩니다.

비슷한 사진 모음에서 카드를 추가하거나 삭제하여 카드 간에 전환을 만들 수 있습니다. Navigation 클래스는 스택에서 카드를 푸시하고 팝하는 함수를 제공합니다. 효과적인 카드 탐색을 빌드하려면 탐색 작업을 사용하도록 위젯을 구성합니다. 여러 카드를 동시에 푸시하거나 팝할 수 있지만 부가기능이 시작될 때 스택에 처음 푸시되는 초기 홈페이지 카드는 삭제할 수 없습니다.

사용자가 위젯과 상호작용하는 것에 대한 응답으로 새 카드로 이동하려면 다음 단계를 따르세요.

  1. Action 객체를 만들고 정의한 콜백 함수와 연결합니다.
  2. 위젯의 적절한 위젯 핸들러 함수를 호출하여 해당 위젯의 Action를 설정합니다.
  3. 탐색을 실행하는 콜백 함수를 구현합니다. 이 함수에는 작업 이벤트 객체가 인수로 제공되며 다음을 실행해야 합니다.
    1. Navigation 객체를 만들어 카드 변경을 정의합니다. 단일 Navigation 객체는 여러 탐색 단계를 포함할 수 있으며, 이러한 단계는 객체에 추가된 순서대로 실행됩니다.
    2. ActionResponseBuilder 클래스와 Navigation 객체를 사용하여 ActionResponse 객체를 빌드합니다.
    3. 빌드된 ActionResponse를 반환합니다.

탐색 컨트롤을 빌드할 때는 다음 Navigation 객체 함수를 사용합니다.

함수 설명
Navigation.pushCard(Card) 카드를 현재 스택에 푸시합니다. 이렇게 하려면 먼저 카드를 완전히 빌드해야 합니다.
Navigation.popCard() 카드 한 장을 삭제합니다. 부가기능 헤더 행에서 뒤로 화살표를 클릭하는 것과 같습니다. 이렇게 해도 루트 카드는 삭제되지 않습니다.
Navigation.popToRoot() 루트 카드를 제외한 모든 카드를 비슷한 카드에서 삭제합니다. 기본적으로 해당 카드 스택을 재설정합니다.
Navigation.popToNamedCard(String) 지정된 이름의 카드 또는 스택의 루트 카드에 도달할 때까지 스택에서 카드를 팝합니다. CardBuilder.setName(String) 함수를 사용하여 카드에 이름을 할당할 수 있습니다.
Navigation.updateCard(Card) 현재 카드를 인플레이스 교체하여 UI에서 카드 표시를 새로고침합니다.

사용자 상호작용이나 이벤트로 인해 동일한 컨텍스트에서 카드가 다시 렌더링되어야 하는 경우 Navigation.pushCard(), Navigation.popCard(), Navigation.updateCard() 메서드를 사용하여 기존 카드를 교체합니다. 사용자 상호작용이나 이벤트로 인해 다른 컨텍스트에서 카드가 다시 렌더링되어야 하는 경우 ActionResponseBuilder.setStateChanged()를 사용하여 이러한 컨텍스트에서 부가기능을 강제로 다시 실행합니다.

다음은 탐색의 예입니다.

  • 상호작용이나 이벤트로 인해 현재 카드의 상태가 변경되는 경우 (예: 할 일 목록에 할 일 추가) updateCard()를 사용합니다.
  • 상호작용이나 이벤트가 추가 세부정보를 제공하거나 사용자에게 추가 작업을 요청하는 경우 (예: 항목 제목을 클릭하여 세부정보를 확인하거나 버튼을 눌러 새 캘린더 일정을 만드는 경우) pushCard()를 사용하여 새 페이지를 표시하면서 사용자가 뒤로 버튼을 사용하여 새 페이지를 종료할 수 있도록 허용합니다.
  • 상호작용 또는 이벤트가 이전 카드의 상태를 업데이트하는 경우 (예: 세부정보 보기에서 상품 제목 업데이트) popCard(), popCard(), pushCard(previous), pushCard(current)와 같은 것을 사용하여 이전 카드와 현재 카드를 업데이트합니다.

카드 새로고침 중

Google Workspace 부가기능을 사용하면 매니페스트에 등록된 Apps Script 트리거 함수를 다시 실행하여 사용자가 카드를 새로고침할 수 있습니다. 사용자는 부가기능 메뉴 항목을 통해 이 새로고침을 트리거합니다.

Google Workspace 부가기능 사이드바

이 작업은 부가기능의 매니페스트 파일 (문맥 및 비문맥 카드 스택의 '루트')에 지정된 대로 homepageTrigger 또는 contextualTrigger 트리거 함수에 의해 생성된 카드에 자동으로 추가됩니다.

여러 카드 반품

부가기능 카드 예시

홈페이지 또는 문맥 트리거 함수는 단일 Card 객체 또는 애플리케이션 UI가 표시하는 Card 객체 배열을 빌드하고 반환하는 데 사용됩니다.

카드가 하나만 있으면 루트 카드로 비컨텍스트 스택 또는 컨텍스트 스택에 추가되고 호스트 애플리케이션 UI에 표시됩니다.

반환된 배열에 빌드된 Card 객체가 두 개 이상 포함된 경우 호스트 애플리케이션은 대신 각 카드의 헤더 목록이 포함된 새 카드를 표시합니다. 사용자가 이러한 헤더 중 하나를 클릭하면 UI에 해당 카드가 표시됩니다.

사용자가 목록에서 카드를 선택하면 해당 카드가 현재 스택에 푸시되고 호스트 애플리케이션에 표시됩니다. 버튼을 누르면 사용자를 카드 헤더 목록으로 되돌립니다.

이 '평면' 카드 배열은 부가기능에서 내가 만든 카드 간에 전환이 필요하지 않은 경우에 적합합니다. 하지만 대부분의 경우 카드 전환을 직접 정의하고 홈페이지 및 문맥 트리거 함수가 단일 카드 객체를 반환하도록 하는 것이 좋습니다.

다음은 카드 간에 이동하는 탐색 버튼이 있는 여러 카드를 구성하는 방법을 보여주는 예입니다. 이러한 카드는 특정 컨텍스트 안팎에서 createNavigationCard()가 반환한 카드를 푸시하여 문맥 스택 또는 비문맥 스택에 추가할 수 있습니다.

  /**
   *  Create the top-level card, with buttons leading to each of three
   *  'children' cards, as well as buttons to backtrack and return to the
   *  root card of the stack.
   *  @return {Card}
   */
  function createNavigationCard() {
    // Create a button set with actions to navigate to 3 different
    // 'children' cards.
    var buttonSet = CardService.newButtonSet();
    for(var i = 1; i <= 3; i++) {
      buttonSet.addButton(createToCardButton(i));
    }

    // Build the card with all the buttons (two rows)
    var card = CardService.newCardBuilder()
        .setHeader(CardService.newCardHeader().setTitle('Navigation'))
        .addSection(CardService.newCardSection()
            .addWidget(buttonSet)
            .addWidget(buildPreviousAndRootButtonSet()));
    return card.build();
  }

  /**
   *  Create a button that navigates to the specified child card.
   *  @return {TextButton}
   */
  function createToCardButton(id) {
    var action = CardService.newAction()
        .setFunctionName('gotoChildCard')
        .setParameters({'id': id.toString()});
    var button = CardService.newTextButton()
        .setText('Card ' + id)
        .setOnClickAction(action);
    return button;
  }

  /**
   *  Create a ButtonSet with two buttons: one that backtracks to the
   *  last card and another that returns to the original (root) card.
   *  @return {ButtonSet}
   */
  function buildPreviousAndRootButtonSet() {
    var previousButton = CardService.newTextButton()
        .setText('Back')
        .setOnClickAction(CardService.newAction()
            .setFunctionName('gotoPreviousCard'));
    var toRootButton = CardService.newTextButton()
        .setText('To Root')
        .setOnClickAction(CardService.newAction()
            .setFunctionName('gotoRootCard'));

    // Return a new ButtonSet containing these two buttons.
    return CardService.newButtonSet()
        .addButton(previousButton)
        .addButton(toRootButton);
  }

  /**
   *  Create a child card, with buttons leading to each of the other
   *  child cards, and then navigate to it.
   *  @param {Object} e object containing the id of the card to build.
   *  @return {ActionResponse}
   */
  function gotoChildCard(e) {
    var id = parseInt(e.parameters.id);  // Current card ID
    var id2 = (id==3) ? 1 : id + 1;      // 2nd card ID
    var id3 = (id==1) ? 3 : id - 1;      // 3rd card ID
    var title = 'CARD ' + id;

    // Create buttons that go to the other two child cards.
    var buttonSet = CardService.newButtonSet()
      .addButton(createToCardButton(id2))
      .addButton(createToCardButton(id3));

    // Build the child card.
    var card = CardService.newCardBuilder()
        .setHeader(CardService.newCardHeader().setTitle(title))
        .addSection(CardService.newCardSection()
            .addWidget(buttonSet)
            .addWidget(buildPreviousAndRootButtonSet()))
        .build();

    // Create a Navigation object to push the card onto the stack.
    // Return a built ActionResponse that uses the navigation object.
    var nav = CardService.newNavigation().pushCard(card);
    return CardService.newActionResponseBuilder()
        .setNavigation(nav)
        .build();
  }

  /**
   *  Pop a card from the stack.
   *  @return {ActionResponse}
   */
  function gotoPreviousCard() {
    var nav = CardService.newNavigation().popCard();
    return CardService.newActionResponseBuilder()
        .setNavigation(nav)
        .build();
  }

  /**
   *  Return to the initial add-on card.
   *  @return {ActionResponse}
   */
  function gotoRootCard() {
    var nav = CardService.newNavigation().popToRoot();
    return CardService.newActionResponseBuilder()
        .setNavigation(nav)
        .build();
  }