스마트 칩으로 링크 미리보기

이 페이지에서는 Google Docs, Sheets, Slides 사용자가 서드 파티 서비스의 링크를 미리 볼 수 있는 Google Workspace 부가기능을 빌드하는 방법을 설명합니다.

Google Workspace 부가기능은 서비스의 링크를 감지하고 사용자에게 미리보기를 요청할 수 있습니다. 지원 케이스, 영업 리드, 직원 프로필 링크와 같은 여러 URL 패턴을 미리 볼 수 있도록 부가기능을 구성할 수 있습니다.

사용자가 링크를 미리 보는 방법

사용자는 링크를 미리 보려면 스마트 칩카드와 상호작용합니다.

사용자가 카드를 미리 봅니다.

사용자가 문서나 스프레드시트에 URL을 입력하거나 붙여넣으면 Google Docs 또는 Google Sheets에서 링크를 스마트 칩으로 대체하라는 메시지가 표시됩니다. 스마트 칩에는 아이콘과 링크 콘텐츠의 짧은 제목 또는 설명이 표시됩니다. 사용자가 칩 위로 마우스를 가져가면 파일 또는 링크에 관한 자세한 정보를 미리 볼 수 있는 카드 인터페이스가 표시됩니다.

다음 동영상에서는 사용자가 링크를 스마트 칩으로 변환하고 카드를 미리 보는 방법을 보여줍니다.

사용자가 Slides에서 링크를 미리 보는 방법

슬라이드의 링크 미리보기에는 서드 파티 스마트 칩이 지원되지 않습니다. 사용자가 프레젠테이션에 URL을 입력하거나 붙여넣으면 슬라이드에서 링크를 칩 대신 제목으로 연결된 텍스트로 대체하라는 메시지가 표시됩니다. 사용자가 링크 제목 위로 마우스를 가져가면 링크에 관한 정보를 미리 볼 수 있는 카드 인터페이스가 표시됩니다.

다음 이미지는 링크 미리보기가 Slides에서 렌더링되는 방식을 보여줍니다.

Slides의 링크 미리보기 예시

기본 요건

Apps Script

Node.js

Python

자바

선택사항: 서드 파티 서비스 인증 설정

부가기능이 승인이 필요한 서비스에 연결되는 경우 사용자는 링크를 미리 보려면 서비스에 인증해야 합니다. 즉, 사용자가 서비스의 링크를 Docs, Sheets 또는 Slides 파일에 처음으로 붙여넣을 때 부가기능에서 승인 흐름을 호출해야 합니다.

OAuth 서비스 또는 맞춤 승인 메시지를 설정하려면 부가기능을 서드 파티 서비스에 연결을 참고하세요.

이 섹션에서는 부가기능의 링크 미리보기를 설정하는 방법을 설명합니다. 여기에는 다음 단계가 포함됩니다.

  1. 부가기능의 매니페스트에서 링크 미리보기를 구성합니다.
  2. 링크의 스마트 칩 및 카드 인터페이스를 빌드합니다.

링크 미리보기 구성

링크 미리보기를 구성하려면 부가기능의 매니페스트에서 다음 섹션과 필드를 지정하세요.

  1. addOns 섹션에서 Docs를 확장하는 docs 필드, Sheets를 확장하는 sheets 필드, Slides를 확장하는 slides 필드를 추가합니다.
  2. 각 필드에서 runFunction가 포함된 linkPreviewTriggers 트리거를 구현합니다. 이 함수는 다음 섹션인 스마트 칩 및 카드 빌드에서 정의합니다.

    linkPreviewTriggers 트리거에서 지정할 수 있는 필드에 대해 알아보려면 Apps Script 매니페스트 또는 기타 런타임의 배포 리소스의 참고 문서를 참고하세요.

  3. 사용자가 링크를 미리 볼 수 있도록 oauthScopes 필드에 https://www.googleapis.com/auth/workspace.linkpreview 범위를 추가하여 부가기능에 권한을 부여할 수 있도록 합니다.

예를 들어 지원 케이스 서비스의 링크 미리보기를 구성하는 다음 매니페스트의 oauthScopesaddons 섹션을 참고하세요.

{
  "oauthScopes": [
    "https://www.googleapis.com/auth/workspace.linkpreview"
  ],
  "addOns": {
    "common": {
      "name": "Preview support cases",
      "logoUrl": "https://www.example.com/images/company-logo.png",
      "layoutProperties": {
        "primaryColor": "#dd4b39"
      }
    },
    "docs": {
      "linkPreviewTriggers": [
        {
          "runFunction": "caseLinkPreview",
          "patterns": [
            {
              "hostPattern": "example.com",
              "pathPrefix": "support/cases"
            },
            {
              "hostPattern": "*.example.com",
              "pathPrefix": "cases"
            },
            {
              "hostPattern": "cases.example.com"
            }
          ],
          "labelText": "Support case",
          "logoUrl": "https://www.example.com/images/support-icon.png",
          "localizedLabelText": {
            "es": "Caso de soporte"
          }
        }
      ]
    },
    "sheets": {
      "linkPreviewTriggers": [
        {
          "runFunction": "caseLinkPreview",
          "patterns": [
            {
              "hostPattern": "example.com",
              "pathPrefix": "support/cases"
            },
            {
              "hostPattern": "*.example.com",
              "pathPrefix": "cases"
            },
            {
              "hostPattern": "cases.example.com"
            }
          ],
          "labelText": "Support case",
          "logoUrl": "https://www.example.com/images/support-icon.png",
          "localizedLabelText": {
            "es": "Caso de soporte"
          }
        }
      ]
    },
    "slides": {
      "linkPreviewTriggers": [
        {
          "runFunction": "caseLinkPreview",
          "patterns": [
            {
              "hostPattern": "example.com",
              "pathPrefix": "support/cases"
            },
            {
              "hostPattern": "*.example.com",
              "pathPrefix": "cases"
            },
            {
              "hostPattern": "cases.example.com"
            }
          ],
          "labelText": "Support case",
          "logoUrl": "https://www.example.com/images/support-icon.png",
          "localizedLabelText": {
            "es": "Caso de soporte"
          }
        }
      ]
    }
  }
}

이 예시에서 Google Workspace 부가기능은 회사의 지원 케이스 서비스 링크를 미리 봅니다. 이 부가기능은 링크를 미리 보려면 세 가지 URL 패턴을 지정합니다. 링크가 URL 패턴 중 하나와 일치할 때마다 콜백 함수 caseLinkPreview가 Docs, Sheets 또는 Slides에 카드와 스마트 칩을 빌드하고 표시하며 URL을 링크 제목으로 대체합니다.

스마트 칩 및 카드 빌드

링크의 스마트 칩과 카드를 반환하려면 linkPreviewTriggers 객체에 지정한 함수를 구현해야 합니다.

사용자가 지정된 URL 패턴과 일치하는 링크와 상호작용할 때 linkPreviewTriggers 트리거가 실행되고 콜백 함수가 이벤트 객체 EDITOR_NAME.matchedUrl.url를 인수로 전달합니다. 이 이벤트 객체의 페이로드를 사용하여 링크 미리보기의 스마트 칩과 카드를 빌드합니다.

예를 들어 사용자가 Docs에서 링크 https://www.example.com/cases/123456를 미리 보는 경우 다음과 같은 이벤트 페이로드가 반환됩니다.

JSON

{
  "docs": {
    "matchedUrl": {
        "url": "https://www.example.com/support/cases/123456"
    }
  }
}

카드 인터페이스를 만들려면 위젯을 사용하여 링크에 관한 정보를 표시합니다. 사용자가 링크를 열거나 콘텐츠를 수정할 수 있는 작업을 빌드할 수도 있습니다. 사용 가능한 위젯 및 작업 목록은 미리보기 카드에 지원되는 구성요소를 참고하세요.

링크 미리보기용 스마트 칩과 카드를 빌드하려면 다음 단계를 따르세요.

  1. 부가기능 매니페스트의 linkPreviewTriggers 섹션에 지정한 함수를 구현합니다.
    1. 이 함수는 EDITOR_NAME.matchedUrl.url가 포함된 이벤트 객체를 인수로 수락하고 단일 Card 객체를 반환해야 합니다.
    2. 서비스에 승인이 필요한 경우 함수도 승인 흐름을 호출해야 합니다.
  2. 미리보기 카드마다 인터페이스에 위젯 상호작용을 제공하는 콜백 함수를 구현합니다. 예를 들어 '링크 보기' 버튼을 포함하는 경우 새 창에서 링크를 여는 콜백 함수를 지정하는 작업을 만들 수 있습니다. 위젯 상호작용에 관한 자세한 내용은 부가기능 작업을 참고하세요.

다음 코드는 Docs용 콜백 함수 caseLinkPreview를 만듭니다.

Apps Script

apps-script/3p-resources/3p-resources.gs
/**
* Entry point for a support case link preview.
*
* @param {!Object} event The event object.
* @return {!Card} The resulting preview link card.
*/
function caseLinkPreview(event) {

  // If the event object URL matches a specified pattern for support case links.
  if (event.docs.matchedUrl.url) {

    // Uses the event object to parse the URL and identify the case details.
    const caseDetails = parseQuery(event.docs.matchedUrl.url);

    // Builds a preview card with the case name, and description
    const caseHeader = CardService.newCardHeader()
      .setTitle(`Case ${caseDetails["name"][0]}`);
    const caseDescription = CardService.newTextParagraph()
      .setText(caseDetails["description"][0]);

    // Returns the card.
    // Uses the text from the card's header for the title of the smart chip.
    return CardService.newCardBuilder()
      .setHeader(caseHeader)
      .addSection(CardService.newCardSection().addWidget(caseDescription))
      .build();
  }
}

/**
* Extracts the URL parameters from the given URL.
*
* @param {!string} url The URL to parse.
* @return {!Map} A map with the extracted URL parameters.
*/
function parseQuery(url) {
  const query = url.split("?")[1];
  if (query) {
    return query.split("&")
    .reduce(function(o, e) {
      var temp = e.split("=");
      var key = temp[0].trim();
      var value = temp[1].trim();
      value = isNaN(value) ? value : Number(value);
      if (o[key]) {
        o[key].push(value);
      } else {
        o[key] = [value];
      }
      return o;
    }, {});
  }
  return null;
}

Node.js

node/3p-resources/index.js
/**
 * 
 * A support case link preview.
 *
 * @param {!URL} url The event object.
 * @return {!Card} The resulting preview link card.
 */
function caseLinkPreview(url) {
  // Builds a preview card with the case name, and description
  // Uses the text from the card's header for the title of the smart chip.
  // Parses the URL and identify the case details.
  const name = `Case ${url.searchParams.get("name")}`;
  return {
    action: {
      linkPreview: {
        title: name,
        previewCard: {
          header: {
            title: name
          },
          sections: [{
            widgets: [{
              textParagraph: {
                text: url.searchParams.get("description")
              }
            }]
          }]
        }
      }
    }
  };
}

Python

python/3p-resources/create_link_preview/main.py
def case_link_preview(url):
    """A support case link preview.
    Args:
      url: A matching URL.
    Returns:
      The resulting preview link card.
    """

    # Parses the URL and identify the case details.
    query_string = parse_qs(url.query)
    name = f'Case {query_string["name"][0]}'
    # Uses the text from the card's header for the title of the smart chip.
    return {
        "action": {
            "linkPreview": {
                "title": name,
                "previewCard": {
                    "header": {
                        "title": name
                    },
                    "sections": [{
                        "widgets": [{
                            "textParagraph": {
                                "text": query_string["description"][0]
                            }
                        }]
                    }],
                }
            }
        }
    }

자바

java/3p-resources/src/main/java/CreateLinkPreview.java
/**
 * A support case link preview.
 *
 * @param url A matching URL.
 * @return The resulting preview link card.
 */
JsonObject caseLinkPreview(URL url) throws UnsupportedEncodingException {
  // Parses the URL and identify the case details.
  Map<String, String> caseDetails = new HashMap<String, String>();
  for (String pair : url.getQuery().split("&")) {
      caseDetails.put(URLDecoder.decode(pair.split("=")[0], "UTF-8"), URLDecoder.decode(pair.split("=")[1], "UTF-8"));
  }

  // Builds a preview card with the case name, and description
  // Uses the text from the card's header for the title of the smart chip.
  JsonObject cardHeader = new JsonObject();
  String caseName = String.format("Case %s", caseDetails.get("name"));
  cardHeader.add("title", new JsonPrimitive(caseName));

  JsonObject textParagraph = new JsonObject();
  textParagraph.add("text", new JsonPrimitive(caseDetails.get("description")));

  JsonObject widget = new JsonObject();
  widget.add("textParagraph", textParagraph);

  JsonArray widgets = new JsonArray();
  widgets.add(widget);

  JsonObject section = new JsonObject();
  section.add("widgets", widgets);

  JsonArray sections = new JsonArray();
  sections.add(section);

  JsonObject previewCard = new JsonObject();
  previewCard.add("header", cardHeader);
  previewCard.add("sections", sections);

  JsonObject linkPreview = new JsonObject();
  linkPreview.add("title", new JsonPrimitive(caseName));
  linkPreview.add("previewCard", previewCard);

  JsonObject action = new JsonObject();
  action.add("linkPreview", linkPreview);

  JsonObject renderActions = new JsonObject();
  renderActions.add("action", action);

  return renderActions;
}

카드 미리보기에 지원되는 구성요소

Google Workspace 부가기능은 링크 미리보기 카드에 다음 위젯과 작업을 지원합니다.

Apps Script

카드 서비스 입력란 유형
TextParagraph 위젯
DecoratedText 위젯
Image 위젯
IconImage 위젯
ButtonSet 위젯
TextButton 위젯
ImageButton 위젯
Grid 위젯
Divider 위젯
OpenLink 작업
Navigation 작업
updateCard 메서드만 지원됩니다.

JSON

카드 (google.apps.card.v1) 필드 유형
TextParagraph 위젯
DecoratedText 위젯
Image 위젯
Icon 위젯
ButtonList 위젯
Button 위젯
Grid 위젯
Divider 위젯
OpenLink 작업
Navigation 작업
updateCard 메서드만 지원됩니다.

전체 예시: 지원 케이스 부가기능

다음 예에서는 Google Docs에서 회사의 지원 케이스 링크를 미리 볼 수 있는 Google Workspace 부가기능을 보여줍니다.

이 예시는 다음을 수행합니다.

  • 지원 케이스 링크(예: https://www.example.com/support/cases/1234)를 미리 봅니다. 스마트 칩에는 지원 아이콘이 표시되고 미리보기 카드에는 케이스 ID와 설명이 포함됩니다.
  • 사용자의 언어가 스페인어로 설정된 경우 스마트 칩은 labelText를 스페인어로 현지화합니다.

매니페스트

Apps Script

apps-script/3p-resources/appsscript.json
{
  "timeZone": "America/New_York",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/workspace.linkpreview",
    "https://www.googleapis.com/auth/workspace.linkcreate"
  ],
  "addOns": {
    "common": {
      "name": "Manage support cases",
      "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png",
      "layoutProperties": {
        "primaryColor": "#dd4b39"
      }
    },
    "docs": {
      "linkPreviewTriggers": [
        {
          "runFunction": "caseLinkPreview",
          "patterns": [
            {
              "hostPattern": "example.com",
              "pathPrefix": "support/cases"
            },
            {
              "hostPattern": "*.example.com",
              "pathPrefix": "cases"
            },
            {
              "hostPattern": "cases.example.com"
            }
          ],
          "labelText": "Support case",
          "localizedLabelText": {
            "es": "Caso de soporte"
          },
          "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png"
        }
      ],
      "createActionTriggers": [
        {
          "id": "createCase",
          "labelText": "Create support case",
          "localizedLabelText": {
            "es": "Crear caso de soporte"
          },
          "runFunction": "createCaseInputCard",
          "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png"
        }
      ]
    }
  }
}

JSON

{
  "oauthScopes": [
    "https://www.googleapis.com/auth/workspace.linkpreview"
  ],
  "addOns": {
    "common": {
      "name": "Preview support cases",
      "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png",
      "layoutProperties": {
        "primaryColor": "#dd4b39"
      }
    },
    "docs": {
      "linkPreviewTriggers": [
        {
          "runFunction": "URL",
          "patterns": [
            {
              "hostPattern": "example.com",
              "pathPrefix": "support/cases"
            },
            {
              "hostPattern": "*.example.com",
              "pathPrefix": "cases"
            },
            {
              "hostPattern": "cases.example.com"
            }
          ],
          "labelText": "Support case",
          "localizedLabelText": {
            "es": "Caso de soporte"
          },
          "logoUrl": "https://developers.google.com/workspace/add-ons/images/support-icon.png"
        }
      ]
    }
  }
}

코드

Apps Script

apps-script/3p-resources/3p-resources.gs
/**
* Entry point for a support case link preview.
*
* @param {!Object} event The event object.
* @return {!Card} The resulting preview link card.
*/
function caseLinkPreview(event) {

  // If the event object URL matches a specified pattern for support case links.
  if (event.docs.matchedUrl.url) {

    // Uses the event object to parse the URL and identify the case details.
    const caseDetails = parseQuery(event.docs.matchedUrl.url);

    // Builds a preview card with the case name, and description
    const caseHeader = CardService.newCardHeader()
      .setTitle(`Case ${caseDetails["name"][0]}`);
    const caseDescription = CardService.newTextParagraph()
      .setText(caseDetails["description"][0]);

    // Returns the card.
    // Uses the text from the card's header for the title of the smart chip.
    return CardService.newCardBuilder()
      .setHeader(caseHeader)
      .addSection(CardService.newCardSection().addWidget(caseDescription))
      .build();
  }
}

/**
* Extracts the URL parameters from the given URL.
*
* @param {!string} url The URL to parse.
* @return {!Map} A map with the extracted URL parameters.
*/
function parseQuery(url) {
  const query = url.split("?")[1];
  if (query) {
    return query.split("&")
    .reduce(function(o, e) {
      var temp = e.split("=");
      var key = temp[0].trim();
      var value = temp[1].trim();
      value = isNaN(value) ? value : Number(value);
      if (o[key]) {
        o[key].push(value);
      } else {
        o[key] = [value];
      }
      return o;
    }, {});
  }
  return null;
}

Node.js

node/3p-resources/index.js
/**
 * Responds to any HTTP request related to link previews.
 *
 * @param {Object} req An HTTP request context.
 * @param {Object} res An HTTP response context.
 */
exports.createLinkPreview = (req, res) => {
  const event = req.body;
  if (event.docs.matchedUrl.url) {
    const url = event.docs.matchedUrl.url;
    const parsedUrl = new URL(url);
    // If the event object URL matches a specified pattern for preview links.
    if (parsedUrl.hostname === 'example.com') {
      if (parsedUrl.pathname.startsWith('/support/cases/')) {
        return res.json(caseLinkPreview(parsedUrl));
      }
    }
  }
};


/**
 * 
 * A support case link preview.
 *
 * @param {!URL} url The event object.
 * @return {!Card} The resulting preview link card.
 */
function caseLinkPreview(url) {
  // Builds a preview card with the case name, and description
  // Uses the text from the card's header for the title of the smart chip.
  // Parses the URL and identify the case details.
  const name = `Case ${url.searchParams.get("name")}`;
  return {
    action: {
      linkPreview: {
        title: name,
        previewCard: {
          header: {
            title: name
          },
          sections: [{
            widgets: [{
              textParagraph: {
                text: url.searchParams.get("description")
              }
            }]
          }]
        }
      }
    }
  };
}

Python

python/3p-resources/create_link_preview/main.py
from typing import Any, Mapping
from urllib.parse import urlparse, parse_qs

import flask
import functions_framework


@functions_framework.http
def create_link_preview(req: flask.Request):
    """Responds to any HTTP request related to link previews.
    Args:
      req: An HTTP request context.
    Returns:
      An HTTP response context.
    """
    event = req.get_json(silent=True)
    if event["docs"]["matchedUrl"]["url"]:
        url = event["docs"]["matchedUrl"]["url"]
        parsed_url = urlparse(url)
        # If the event object URL matches a specified pattern for preview links.
        if parsed_url.hostname == "example.com":
            if parsed_url.path.startswith("/support/cases/"):
                return case_link_preview(parsed_url)

    return {}




def case_link_preview(url):
    """A support case link preview.
    Args:
      url: A matching URL.
    Returns:
      The resulting preview link card.
    """

    # Parses the URL and identify the case details.
    query_string = parse_qs(url.query)
    name = f'Case {query_string["name"][0]}'
    # Uses the text from the card's header for the title of the smart chip.
    return {
        "action": {
            "linkPreview": {
                "title": name,
                "previewCard": {
                    "header": {
                        "title": name
                    },
                    "sections": [{
                        "widgets": [{
                            "textParagraph": {
                                "text": query_string["description"][0]
                            }
                        }]
                    }],
                }
            }
        }
    }

자바

java/3p-resources/src/main/java/CreateLinkPreview.java
import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;

public class CreateLinkPreview implements HttpFunction {
  private static final Gson gson = new Gson();

  /**
   * Responds to any HTTP request related to link previews.
   *
   * @param request An HTTP request context.
   * @param response An HTTP response context.
   */
  @Override
  public void service(HttpRequest request, HttpResponse response) throws Exception {
    JsonObject event = gson.fromJson(request.getReader(), JsonObject.class);
    String url = event.getAsJsonObject("docs")
        .getAsJsonObject("matchedUrl")
        .get("url")
        .getAsString();
    URL parsedURL = new URL(url);
    // If the event object URL matches a specified pattern for preview links.
    if ("example.com".equals(parsedURL.getHost())) {
      if (parsedURL.getPath().startsWith("/support/cases/")) {
        response.getWriter().write(gson.toJson(caseLinkPreview(parsedURL)));
        return;
      }
    }

    response.getWriter().write("{}");
  }


  /**
   * A support case link preview.
   *
   * @param url A matching URL.
   * @return The resulting preview link card.
   */
  JsonObject caseLinkPreview(URL url) throws UnsupportedEncodingException {
    // Parses the URL and identify the case details.
    Map<String, String> caseDetails = new HashMap<String, String>();
    for (String pair : url.getQuery().split("&")) {
        caseDetails.put(URLDecoder.decode(pair.split("=")[0], "UTF-8"), URLDecoder.decode(pair.split("=")[1], "UTF-8"));
    }

    // Builds a preview card with the case name, and description
    // Uses the text from the card's header for the title of the smart chip.
    JsonObject cardHeader = new JsonObject();
    String caseName = String.format("Case %s", caseDetails.get("name"));
    cardHeader.add("title", new JsonPrimitive(caseName));

    JsonObject textParagraph = new JsonObject();
    textParagraph.add("text", new JsonPrimitive(caseDetails.get("description")));

    JsonObject widget = new JsonObject();
    widget.add("textParagraph", textParagraph);

    JsonArray widgets = new JsonArray();
    widgets.add(widget);

    JsonObject section = new JsonObject();
    section.add("widgets", widgets);

    JsonArray sections = new JsonArray();
    sections.add(section);

    JsonObject previewCard = new JsonObject();
    previewCard.add("header", cardHeader);
    previewCard.add("sections", sections);

    JsonObject linkPreview = new JsonObject();
    linkPreview.add("title", new JsonPrimitive(caseName));
    linkPreview.add("previewCard", previewCard);

    JsonObject action = new JsonObject();
    action.add("linkPreview", linkPreview);

    JsonObject renderActions = new JsonObject();
    renderActions.add("action", action);

    return renderActions;
  }

}