맞춤 Workspace Studio 시작 조건 및 작업 단계 빌드

1. 소개

이 Codelab에서는 Google Workspace Studio와 직접 인터페이스하는 대화형 지원 빠른 포털을 빌드합니다. 서드 파티 인프라에 의존하지 않고 즉시 테스트할 수 있는 맞춤 시작 단계와 맞춤 작업 단계를 빌드하는 방법을 알아봅니다.

Google Apps Script를 사용하여 다음을 만듭니다.

  • 사용자가 지원 티켓을 수동으로 제출할 수 있는 세련된 양식이 있는 웹 앱
  • 이러한 웹 앱 제출을 캡처하고 Workspace Studio 플로우를 트리거하는 맞춤 시작 조건
  • 들어오는 티켓의 설명을 분석하여 긴급도 수준 ('높음' 또는 '보통')을 식별하는 맞춤 단계

학습할 내용

  • appsscript.json 매니페스트에서 스타터와 단계를 구성하는 방법
  • 시작 프로그램 등록 수명 주기 중에 notifyUri를 구성하는 방법
  • UrlFetchApp를 사용하여 외부 코드에서 Workspace Studio 플로우를 프로그래매틱 방식으로 트리거하는 방법
  • 데이터를 다운스트림으로 전달하기 위해 맞춤 단계에서 출력 변수를 구성하는 방법

기본 요건

  • Google Workspace Studio가 사용 설정된 Google Workspace 계정
  • 도메인의 관리 콘솔 (앱 > Google Workspace > Workspace Studio > 맞춤 단계 설정)에서 게시되지 않은 (테스트) 맞춤 단계 허용 설정이 사용 설정되어 있습니다.
  • Google Apps Script에 대한 지식

2. Apps Script 프로젝트 설정

먼저 코드를 저장할 새 Apps Script 프로젝트를 만듭니다.

  1. script.google.com으로 이동하여 새 프로젝트를 클릭합니다.
  2. 프로젝트 이름을 Support Quick Portal로 지정합니다.
  3. 왼쪽 사이드바에서 프로젝트 설정 프로젝트 설정을 클릭합니다.
  4. 편집기에 appsscript.json 매니페스트 파일 표시 체크박스를 선택합니다.
  5. 편집자 편집기 보기로 돌아갑니다.

기본적으로 Apps Script 프로젝트는 숨겨진 기본 Google Cloud 프로젝트와 연결됩니다. Google Workspace Studio API를 호출하고 PERMISSION_DENIED 오류를 방지하려면 스크립트를 표준 Cloud 프로젝트로 전환하고 API를 사용 설정하고 Google 인증 플랫폼을 올바르게 구성해야 합니다.

  1. 프로젝트 선택 또는 만들기: Google Cloud 콘솔로 이동하여 페이지 상단의 프로젝트 드롭다운 메뉴를 사용하여 기존 프로젝트를 선택하거나 새 프로젝트를 만듭니다.
  2. API 사용 설정: 선택한 프로젝트가 상단 표시줄에서 활성화되면 API 사용 설정 흐름을 열고 Workspace Studio API에 대해 다음 및 사용 설정을 클릭합니다.
  3. OAuth 브랜딩 구성: Google 인증 플랫폼 브랜딩 페이지를 엽니다.
    1. 메시지가 표시되면 시작하기를 클릭합니다. (이미 구성된 경우 4단계로 건너뜁니다.)
    2. 앱 정보에 앱 이름과 사용자 지원 이메일을 입력합니다. 다음을 클릭합니다.
    3. 대상에서 내부 (또는 내부를 사용할 수 없는 경우 외부)를 선택하고 다음을 클릭합니다.
    4. 연락처 정보를 제공하고 데이터 정책에 동의한 후 만들기를 클릭합니다.
  4. 데이터 액세스 구성: 데이터 액세스 페이지를 열고 범위 추가 또는 삭제를 클릭합니다.
    1. 직접 범위 추가에서 https://www.googleapis.com/auth/workspace.studio.trigger를 붙여넣습니다.
    2. 표에 추가를 클릭한 다음 업데이트를 클릭하고 마지막으로 페이지 하단의 저장을 클릭합니다.
  5. IAM 및 관리자 > 설정 (또는 Cloud 콘솔 대시보드)으로 이동하여 프로젝트 번호를 복사합니다.
  6. Apps Script 프로젝트로 돌아가 왼쪽 사이드바에서 프로젝트 설정 프로젝트 설정을 클릭합니다.
  7. Google Cloud Platform (GCP) 프로젝트 섹션에서 프로젝트 변경을 클릭합니다.
  8. 복사한 GCP 프로젝트 번호를 입력하고 프로젝트 설정을 클릭합니다.

매니페스트 구성

새로 표시된 appsscript.json 파일을 엽니다. 파일의 콘텐츠를 아래 코드로 바꿉니다. 이 매니페스트는 필수 OAuth 범위를 명시적으로 나열하고 studio.flows.workflowElements 블록 아래에서 시작 조건을 workflowTrigger로, 작업을 workflowAction로 정의합니다.

{
  "timeZone": "America/Los_Angeles",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "webapp": {
     "executeAs": "USER_ACCESSING",
     "access": "MYSELF"
  },
  "oauthScopes": [
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/workspace.studio.trigger",
    "https://www.googleapis.com/auth/script.storage",
    "https://www.googleapis.com/auth/script.locale"
  ],
  "urlFetchWhitelist": [
    "https://workspacestudio.googleapis.com/"
  ],
  "addOns": {
    "common": {
      "name": "Support Extension",
      "logoUrl": "https://www.gstatic.com/images/branding/productlogos/buganizer/v1/192px.svg",
      "useLocaleFromApp": true
    },
    "studio": {
      "flows": {
        "workflowElements": [
          {
            "id": "supportTicketTrigger",
            "state": "ACTIVE",
            "name": "New Support Ticket",
            "description": "Fires when someone submits a ticket via the Web App.",
            "workflowTrigger": {
              "inputs": [],
              "outputs": [
                {
                  "id": "ticketTitle",
                  "description": "The title of the ticket",
                  "cardinality": "SINGLE",
                  "dataType": { "basicType": "STRING" }
                },
                {
                  "id": "ticketDescription",
                  "description": "The full description of the issue",
                  "cardinality": "SINGLE",
                  "dataType": { "basicType": "STRING" }
                }
              ],
              "onConfigFunction": "onConfigTrigger",
              "onManageFunction": "onManageTrigger"
            }
          },
          {
            "id": "urgencyDetectorStep",
            "state": "ACTIVE",
            "name": "Detect Urgency",
            "description": "Scans the ticket description to detect High or Normal urgency.",
            "workflowAction": {
              "inputs": [
                {
                  "id": "ticketDescription",
                  "description": "The ticket description",
                  "cardinality": "SINGLE",
                  "dataType": { "basicType": "STRING" }
                }
              ],
              "outputs": [
                {
                  "id": "urgencyLevel",
                  "description": "The urgency High/Normal",
                  "cardinality": "SINGLE",
                  "dataType": { "basicType": "STRING" }
                }
              ],
              "onConfigFunction": "onConfigUrgency",
              "onExecuteFunction": "onExecuteUrgency"
            }
          }
        ]
      }
    }
  }
}

3. 웹 앱 UI 빌드

터미널 도구나 서드 파티 웹훅을 사용하지 않고 시각적이고 테스트 가능한 방식으로 스타터를 트리거하기 위해 Apps Script 웹 앱을 빌드합니다.

Apps Script 편집기에서 파일 옆에 있는 파일 추가 파일 추가를 클릭하고 HTML을 선택합니다. 이름을 index.html로 지정합니다 (.html 확장 프로그램은 자동으로 추가됨).

다음 간소화된 UI 코드를 붙여넣습니다.

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <style>
      body { font-family: Arial, sans-serif; padding: 20px; max-width: 500px; margin: auto; }
      label { font-weight: bold; display: block; margin-top: 15px; }
      input, textarea { width: 100%; margin-top: 5px; padding: 10px; box-sizing: border-box; }
      button { background-color: #1a73e8; color: white; border: none; padding: 10px 15px; margin-top: 15px; font-weight: bold; cursor: pointer; border-radius: 4px; }
      button:hover { background-color: #1557b0; }
      #status { margin-top: 15px; color: green; font-weight: bold; }
    </style>
  </head>
  <body>
    <h2>Support Quick Portal</h2>
    <p>Submit your issue below to trigger the Workspace Studio flow.</p>

    <label for="title">Ticket Title</label>
    <input type="text" id="title" placeholder="e.g. Broken monitor">

    <label for="description">Issue Description</label>
    <textarea id="description" rows="4" placeholder="Explain your issue (use words like 'urgent' to test the step!)..."></textarea>

    <button onclick="submitTicket()">Submit Ticket</button>
    <div id="status"></div>

    <script>
      function submitTicket() {
        const title = document.getElementById('title').value;
        const description = document.getElementById('description').value;
        const statusEl = document.getElementById('status');

        if (!title || !description) {
           statusEl.style.color = "red";
           statusEl.innerText = "Please fill out both fields.";
           return;
        }

        statusEl.style.color = "green";
        statusEl.innerText = "Submitting ticket...";

        google.script.run
          .withSuccessHandler(function(response) {
            statusEl.innerText = response;
          })
          .withFailureHandler(function(error) {
            statusEl.style.color = 'red';
            statusEl.innerText = 'Error: ' + error.message;
          })
          .submitTicketToStudio(title, description);
      }
    </script>
  </body>
</html>

4. 백엔드 스크립트 빌드

이제 모든 것을 연결해 보겠습니다. 코드를 읽기 쉽게 유지하기 위해 웹 앱, 시작 프로그램 로직, 작업 단계 로직을 세 개의 서로 다른 스크립트 파일로 분리합니다.

WebApp.gs

편집기에서 기본 Code.gs 파일의 이름을 WebApp.gs로 바꾸고 상용구 myFunction를 삭제한 다음 아래 블록을 복사하여 붙여넣습니다. 이 파일은 웹 앱 호스팅을 처리합니다.

/** --- WEB APP HOSTING --- **/

/**
 * Serves the HTML UI when users visit the web app URL.
 */
function doGet() {
  return HtmlService.createHtmlOutputFromFile('index')
      .setTitle('Support Quick Portal')
      .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

/**
 * Called by the Web App form. It calls the Workspace Studio API.
 */
function submitTicketToStudio(title, description) {
  const props = PropertiesService.getUserProperties();
  const notifyUri = props.getProperty('notifyUri');
  const triggerId = props.getProperty('triggerId');

  if (!notifyUri || !triggerId) {
    throw new Error('Trigger URL missing! Make sure the flow is enabled in Workspace Studio.');
  }

  // Use ScriptApp.getOAuthToken() (requires the workspace.studio.trigger scope in manifest)
  const token = ScriptApp.getOAuthToken();
  const requestId = Utilities.getUuid();

  // Package our outputs mapped in appsscript.json
  const payload = {
    "name": "triggers/" + triggerId,
    "outputs": {
      "ticketTitle": { "stringValues": [title] },
      "ticketDescription": { "stringValues": [description] }
    },
    "requestId": requestId
  };

  const options = {
    "method": "POST",
    "contentType": "application/json",
    "headers": { "Authorization": "Bearer " + token },
    "payload": JSON.stringify(payload),
    "muteHttpExceptions": true
  };

  // Fire!
  const response = UrlFetchApp.fetch(notifyUri, options);
  if (response.getResponseCode() !== 200) {
    console.error(response.getContentText());
    throw new Error('Workspace Studio API Error: ' + response.getResponseCode());
  }
  return 'Ticket successfully routed to Workspace Studio!';
}

Starter.gs

그런 다음 파일 위로 마우스를 가져가 파일 추가 파일 추가를 클릭하고 스크립트를 선택한 후 Starter.gs라고 이름을 지정합니다. 아래 블록을 복사하여 붙여넣습니다. 이 파일은 스타터 구성과 Workspace Studio 수명 주기 관리를 처리합니다.

/** --- STARTER LOGIC --- **/

/**
 * Workspace Studio calls this to display the config UI when the user adds the starter.
 * We just return a static message since there are no properties to configure.
 */
function onConfigTrigger() {
  const section = CardService.newCardSection()
      .setHeader("Configure Web App Starter")
      .addWidget(CardService.newTextParagraph().setText("Ready to go! Once you enable this flow, deploy your Apps Script project as a Web App to submit tickets."));

  return CardService.newCardBuilder().addSection(section).build();
}

/**
 * Workspace Studio posts data here to manage the lifecycle (enable/disable the trigger).
 * Critically, we must construct or save the `notifyUri` and `triggerId` to call it later.
 */
function onManageTrigger(event) {
  const triggerCreation = event.workflow.triggerCreation;
  const triggerDeletion = event.workflow.triggerDeletion;

  const props = PropertiesService.getUserProperties();

  if (triggerCreation) {
    // Save the unique URL and ID when the flow is enabled
    // Note: notifyUri might be omitted, so fallback to constructing it dynamically
    const notifyUri = triggerCreation.notifyUri || `https://workspacestudio.googleapis.com/v1/triggers/${triggerCreation.triggerId}:fire`;
    props.setProperty('notifyUri', notifyUri);
    props.setProperty('triggerId', triggerCreation.triggerId);
  } else if (triggerDeletion) {
    // Cleanup if the user deletes the flow
    props.deleteProperty('notifyUri');
    props.deleteProperty('triggerId');
  }
}

Action.gs

그런 다음 파일 위로 마우스를 가져가 파일 추가 파일 추가를 클릭하고 스크립트를 선택한 후 Action.gs라고 이름을 지정합니다. 아래 긴급성 단계 로직을 복사하여 붙여넣습니다.

/** --- ACTION STEP LOGIC --- **/

/**
 * Workspace Studio configuration card for the Step.
 * We prompt the user to bind a variable to our "ticketDescription" input.
 */
function onConfigUrgency() {
  const input = CardService.newTextInput()
      .setFieldName("ticketDescription")
      .setTitle("Source Description")
      .setHint('Select the description variable outputted by the starter')
      .setHostAppDataSource(
        CardService.newHostAppDataSource().setWorkflowDataSource(CardService.newWorkflowDataSource().setIncludeVariables(true))
      );

  const section = CardService.newCardSection()
      .setHeader("Urgency Detector Step")
      .addWidget(input);

  return CardService.newCardBuilder().addSection(section).build();
}

/**
 * The synchronous logic executed during the Step.
 */
function onExecuteUrgency(event) {
  // Read mapped inputs
  let description = event.workflow.actionInvocation.inputs["ticketDescription"].stringValues[0];
  if (!description) description = "";

  const urgentKeywords = ['urgent', 'crash', 'broken', 'down', 'fire'];
  let urgencyLevel = 'Normal';

  if (urgentKeywords.some(keyword => description.toLowerCase().includes(keyword))) {
    urgencyLevel = 'High';
  }

  // Package outputs correctly
  const variableDataMap = {
    "urgencyLevel": AddOnsResponseService.newVariableData().addStringValue(urgencyLevel)
  };

  return outputVariables(variableDataMap);
}

/**
 * Helper strictly enforcing the expected Step object return type.
 */
function outputVariables(variableDataMap) {
  const workflowAction = AddOnsResponseService.newReturnOutputVariablesAction()
      .setVariableDataMap(variableDataMap);
  const hostAppAction = AddOnsResponseService.newHostAppAction()
      .setWorkflowAction(workflowAction);
  return AddOnsResponseService.newRenderActionBuilder()
      .setHostAppAction(hostAppAction)
      .build();
}

5. 배포 및 테스트

코드를 작성했으므로 프로젝트를 배포하고 구성해 보겠습니다.

1단계: 스크립트 다시 승인하기

새 표준 Google Cloud 프로젝트를 연결했으므로 새 프로젝트에 대해 스크립트의 범위 (예: UrlFetchApp에서 사용하는 script.external_request)를 명시적으로 승인해야 합니다.

  1. Apps Script 스크립트 편집기에서 파일 아래의 WebApp.gs를 선택한 다음 상단 툴바 드롭다운에서 doGet 함수를 선택합니다.
  2. 실행을 클릭합니다.
  3. '승인 필요'라는 메시지가 표시됩니다. 권한 검토를 클릭합니다.
  4. 계정을 선택하고 스크립트를 승인합니다. '인증되지 않은 앱' 경고가 표시되면 고급 -> 지원 빠른 포털로 이동을 클릭하여 무시합니다.
  5. 실행 로그에 실행 완료가 표시될 때까지 기다립니다. 그러면 스크립트가 완전히 승인된 것입니다.

2단계: 확장 프로그램 및 웹 앱 테스트

appsscript.json는 부가기능과 웹 앱을 모두 정의하므로 두 가지를 동시에 구성할 수 있습니다.

  1. Apps Script 오른쪽 상단에서 배포 > 테스트 배포를 클릭합니다.
  2. 유형 선택 옆에 있는 배포 유형 사용 설정 배포 유형 사용 설정을 클릭하고 Google Workspace 부가기능이 선택되어 있는지 확인한 다음 설치를 클릭합니다.
  3. 배포 유형 사용 설정 배포 유형 사용 설정을 다시 클릭하고 웹 앱이 선택되어 있는지 확인한 후 URL을 복사합니다 (/dev로 끝남). 이는 빌드한 지원 빠른 포털 웹 앱의 URL입니다.
  4. 완료를 클릭합니다. Workspace Studio에서 부가기능을 등록하기 전에 브라우저를 새로고침해야 할 수 있습니다.

3단계: 스튜디오 흐름 구성

  1. studio.workspace.google.com에서 Google Workspace Studio를 엽니다.
  2. 왼쪽에서 도구 추가 도구 추가를 클릭하고 흐름을 선택합니다.
  3. 시작 조건을 구성하려면 시작 조건 선택을 클릭한 다음 설치된 지원 확장 프로그램 > 새 지원 티켓을 선택합니다. '권한 필요' 메시지가 표시되면 권한 부여를 클릭하여 부가기능이 데이터에 액세스하도록 허용합니다.
  4. 다음으로 '작업'에서 단계 선택을 클릭하고 '지원 확장 프로그램'으로 스크롤한 다음 긴급성 감지 단계를 선택합니다.
  5. 소스 설명에서 추가 변수를 클릭하고 1단계: 새 지원 티켓 > 문제의 전체 설명 (ticketDescription)을 선택합니다.
  6. 단계 선택을 다시 클릭하고 Gmail > 이메일 보내기 또는 Chat > Chat에서 알림 받기와 같은 기본 제공 단계를 선택합니다.
    • Gmail을 선택한 경우:
      • 받는사람: 내 이메일을 입력합니다.
      • 제목: 티켓 ticketTitle 우선순위는 urgencyLevel입니다.
      • 메시지: ticketDescription
    • 채팅을 선택한 경우:
      • 메시지: 티켓 ticketTitle의 우선순위는 urgencyLevel입니다. ticketDescription
  7. 워크플로를 사용 설정합니다. 플로우 이름을 바꾸라는 메시지가 표시되면 Support Ticket Router와 같은 이름을 입력하고 저장합니다. 이렇게 하면 onManageTrigger 수명 주기가 트리거되어 notifyUri가 저장됩니다.

4단계: 시뮬레이션 실행

  1. 이 페이지의 2단계에서 복사한 웹 앱 URL을 새 브라우저 탭에서 엽니다(/dev로 끝남).
  2. 양식을 작성합니다. 설명에 트리거 키워드('긴급' 또는 '비정상 종료' 등)를 포함해야 합니다.
  3. 제출을 클릭합니다. '티켓이 성공적으로 라우팅됨' 메시지가 표시될 때까지 기다립니다.
  4. 선택한 작업에 따라 Workspace Studio 활동 로그, Gmail 받은편지함 또는 Google Chat을 확인합니다. 맞춤 Apps Script 요소를 사용하여 기본적으로 통합된 '높음' 긴급성 태그가 지정된 메시지 또는 이메일이 표시됩니다.

6. 삭제

작업공간과 Google Cloud 계정이 어수선해지지 않도록 이 Codelab에서 만든 리소스를 정리할 수 있습니다.

  1. Workspace Studio 플로우 삭제:
    • studio.workspace.google.com을 엽니다.
    • Support Ticket Router 흐름을 찾으세요.
    • 옆에 있는 점 3개 메뉴를 클릭하고 삭제를 선택합니다.
  2. Apps Script 프로젝트 삭제:
    • script.google.com으로 이동합니다.
    • Support Quick Portal 프로젝트를 찾습니다.
    • 점 3개로 된 메뉴를 클릭하고 삭제를 선택합니다.
  3. Google Cloud 프로젝트 종료:
    • Google Cloud 콘솔로 이동합니다.
    • 상단의 드롭다운에서 새 프로젝트가 선택되어 있는지 확인합니다.
    • IAM 및 관리자 > 설정으로 이동하여 종료를 클릭합니다.

7. 결론

외부 서드 파티 구독에 의존하지 않고 Google Workspace Studio 맞춤 확장 프로그램을 위한 완전한 생태계를 구축했습니다.

다룬 내용:

  • workflowTrigger를 onManageFunction와 함께 사용하여 Google의 수명 주기 이벤트를 올바르게 캡처하고 처리합니다.
  • workspacestudio.googleapis.com API 엔드포인트에 대해 ScriptApp.getOAuthToken()을 사용하여 백엔드 요청을 인증합니다.
  • workflowAction를 사용하여 입력 변수를 동기식으로 처리하고 다운스트림 출력을 생성합니다.

이제 웹 앱 (또는 수신 웹훅)이 트리거되는 위치를 수정하여 이러한 원칙을 수평 확장하여 Jira, Salesforce 또는 Zendesk와 같은 실제 SaaS 플랫폼에 연결할 수 있습니다.