여러 문서의 콘텐츠 집계

코딩 수준: 초급
소요 시간: 15분
프로젝트 유형: 맞춤 메뉴를 사용한 자동화

목표

  • 솔루션의 작동 방식을 이해합니다.
  • 솔루션 내에서 Apps Script 서비스가 하는 작업을 이해합니다.
  • 스크립트를 설정합니다.
  • 스크립트를 실행합니다.

이 솔루션 정보

수동으로 복사하여 붙여넣을 때 발생하는 시간을 절약하고 오류를 줄이려면 여러 문서의 콘텐츠를 하나의 기본 문서로 자동으로 가져올 수 있습니다. 이 솔루션은 프로젝트 상태 보고서를 집계하는 데 중점을 두지만 필요에 맞게 수정할 수 있습니다.

가져온 프로젝트 요약 스크린샷

작동 방식

이 스크립트는 콘텐츠를 가져올 문서를 저장할 폴더와 가져오기를 시작할 템플릿 문서를 만듭니다. 이 솔루션을 데모하기 위한 샘플 문서를 만드는 함수도 스크립트에 포함되어 있습니다.

사용자가 맞춤 메뉴에서 요약 가져오기를 선택하면 스크립트가 폴더의 모든 Docs 파일을 가져와 각각을 반복합니다. 스크립트는 특정 문자열과 제목 유형을 찾아 복사해야 하는 요약 텍스트를 식별합니다. 텍스트가 복사된 후 스크립트는 중복을 완화하기 위해 식별자 문자열의 텍스트 색상을 변경합니다. 이 스크립트는 요약을 각각 자체 단일 셀 테이블로 기본 문서에 붙여넣습니다.

Apps Script 서비스

이 솔루션은 다음 서비스를 사용합니다.

  • 문서 서비스: 템플릿과 샘플 소스 문서를 만듭니다. 가져올 새 프로젝트 요약을 찾기 위해 각 소스 문서를 반복합니다. 요약을 기본 문서로 가져옵니다. 요약이 두 번 이상 가져오지 않도록 소스 문서를 업데이트합니다.
  • Drive 서비스: 소스 문서를 저장할 폴더를 만듭니다. 템플릿 문서와 샘플 소스 문서를 폴더에 추가합니다.
  • 유틸리티 서비스: 스크립트가 소스 문서에서 요약을 가져올 때마다 스크립트가 기본 문서에 추가하는 날짜의 형식을 지정합니다.
  • 기본 서비스: Session 클래스를 사용하여 스크립트의 시간대를 가져옵니다. 스크립트는 기본 문서에 가져온 날짜를 추가할 때 시간대를 사용합니다.

기본 요건

이 샘플을 사용하려면 다음 기본 요건이 필요합니다.

  • Google 계정 (Google Workspace 계정의 경우 관리자 승인이 필요할 수 있음)
  • 인터넷에 액세스할 수 있는 웹브라우저

스크립트 설정

아래 버튼을 클릭하여 콘텐츠 집계 문서의 사본을 만드세요.
사본 만들기

스크립트 실행

샘플 문서로 데모 실행

  1. 요약 가져오기 > 구성 > 샘플 문서로 데모 설정 실행을 클릭합니다. 이 맞춤 메뉴가 표시되도록 페이지를 새로고침해야 할 수도 있습니다.
  2. 메시지가 표시되면 스크립트를 승인합니다. OAuth 동의 화면에 이 앱은 확인되지 않았습니다라는 경고가 표시되면 고급 > {프로젝트 이름}으로 이동(안전하지 않음)을 선택하여 계속 진행합니다.

  3. 요약 가져오기 > 구성 > 샘플 문서를 사용하여 데모 설정 실행을 다시 클릭합니다.

  4. 메시지가 표시되면 나중에 사용할 Drive 폴더의 URL을 복사합니다.

  5. 확인을 클릭합니다.

  6. 요약 가져오기 > 요약 가져오기를 클릭합니다.

  7. 메시지가 표시되면 확인을 클릭합니다.

  8. 샘플 문서에서 가져온 프로젝트 요약을 검토합니다.

요약 추가 및 가져오기

  1. 새 브라우저 탭에서 폴더 URL을 붙여넣어 Project status(프로젝트 상태) 폴더를 엽니다.
  2. Project ABC 파일을 엽니다.
  3. 문서 끝에 다음 콘텐츠를 추가하여 가져올 새 요약을 만듭니다.
    1. Summary를 입력하고 텍스트 스타일을 제목 3으로 설정합니다.
    2. Summary 바로 아래에 1x1 표를 삽입합니다. Summary와 테이블 사이에 공백 줄이 없어야 합니다.
    3. 표에 Hello world!를 입력합니다.
  4. 기본 문서로 다시 전환하고 요약 가져오기 > 요약 가져오기를 클릭합니다.
  5. 메시지가 표시되면 확인을 클릭합니다.
  6. 문서 끝에 있는 최신 가져온 항목을 확인합니다.

코드 검토

이 솔루션의 Apps Script 코드를 검토하려면 아래의 소스 코드 보기를 클릭합니다.

소스 코드 보기

Code.gs

solutions/automations/aggregate-document-content/Code.js
// To learn how to use this script, refer to the documentation:
// https://developers.google.com/apps-script/samples/automations/aggregate-document-content

/*
Copyright 2022 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/** 
 * This file containts the main application functions that import data from
 * summary documents into the body of the main document.
 */

// Application constants
const APP_TITLE = 'Document summary importer'; // Application name
const PROJECT_FOLDER_NAME = 'Project statuses'; // Drive folder for the source files.

// Below are the parameters used to identify which content to import from the source documents
// and which content has already been imported.
const FIND_TEXT_KEYWORDS = 'Summary'; // String that must be found in the heading above the table (case insensitive).
const APP_STYLE = DocumentApp.ParagraphHeading.HEADING3; // Style that must be applied to heading above the table.
const TEXT_COLOR = '#2e7d32'; // Color applied to heading after import to avoid duplication.

/**
 * Updates the main document, importing content from the source files.
 * Uses the above parameters to locate content to be imported.
 * 
 * Called from menu option.
 */
function performImport() {
  // Gets the folder in Drive associated with this application.
  const folder = getFolderByName_(PROJECT_FOLDER_NAME);
  // Gets the Google Docs files found in the folder. 
  const files = getFiles(folder);

  // Warns the user if the folder is empty.
  const ui = DocumentApp.getUi();
  if (files.length === 0) {
    const msg =
      `No files found in the folder '${PROJECT_FOLDER_NAME}'.
      Run '${MENU.SETUP}' | '${MENU.SAMPLES}' from the menu
      if you'd like to create samples files.`
    ui.alert(APP_TITLE, msg, ui.ButtonSet.OK);
    return;
  }

  /** Processes main document */
  // Gets the active document and body section.
  const docTarget = DocumentApp.getActiveDocument();
  const docTargetBody = docTarget.getBody();

  // Appends import summary section to the end of the target document. 
  // Adds a horizontal line and a header with today's date and a title string.
  docTargetBody.appendHorizontalRule();
  const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy');
  const headingText = `Imported: ${dateString}`;
  docTargetBody.appendParagraph(headingText).setHeading(APP_STYLE);
  // Appends a blank paragraph for spacing.
  docTargetBody.appendParagraph(" ");

  /** Process source documents */
  // Iterates through each source document in the folder.
  // Copies and pastes new updates to the main document.
  let noContentList = [];
  let numUpdates = 0;
  for (let id of files) {

    // Opens source document; get info and body.
    const docOpen = DocumentApp.openById(id);
    const docName = docOpen.getName();
    const docHtml = docOpen.getUrl();
    const docBody = docOpen.getBody();

    // Gets summary content from document and returns as object {content:content}
    const content = getContent(docBody);

    // Logs if document doesn't contain content to be imported.
    if (!content) {
      noContentList.push(docName);
      continue;
    }
    else {
      numUpdates++
      // Inserts content into the main document.
      // Appends a title/url reference link back to source document.
      docTargetBody.appendParagraph('').appendText(`${docName}`).setLinkUrl(docHtml);
      // Appends a single-cell table and pastes the content.
      docTargetBody.appendTable(content);
    }
    docOpen.saveAndClose()
  }
  /** Provides an import summary */
  docTarget.saveAndClose();
  let msg = `Number of documents updated: ${numUpdates}`
  if (noContentList.length != 0) {
    msg += `\n\nThe following documents had no updates:`
    for (let file of noContentList) {
      msg += `\n ${file}`;
    }
  }
  ui.alert(APP_TITLE, msg, ui.ButtonSet.OK);
}

/**
 * Updates the main document drawing content from source files.
 * Uses the parameters at the top of this file to locate content to import.
 * 
 * Called from performImport().
 */
function getContent(body) {

  // Finds the heading paragraph with matching style, keywords and !color.
  var parValidHeading;
  const searchType = DocumentApp.ElementType.PARAGRAPH;
  const searchHeading = APP_STYLE;
  let searchResult = null;

  // Gets and loops through all paragraphs that match the style of APP_STYLE.
  while (searchResult = body.findElement(searchType, searchResult)) {
    let par = searchResult.getElement().asParagraph();
    if (par.getHeading() == searchHeading) {
      // If heading style matches, searches for text string (case insensitive).
      let findPos = par.findText('(?i)' + FIND_TEXT_KEYWORDS);
      if (findPos !== null) {

        // If text color is green, then the paragraph isn't a new summary to copy.
        if (par.editAsText().getForegroundColor() != TEXT_COLOR) {
          parValidHeading = par;
        }
      }
    }
  }

  if (!parValidHeading) {
    return;
  } else {
    // Updates the heading color to indicate that the summary has been imported.     
    let style = {};
    style[DocumentApp.Attribute.FOREGROUND_COLOR] = TEXT_COLOR;
    parValidHeading.setAttributes(style);
    parValidHeading.appendText(" [Exported]");

    // Gets the content from the table following the valid heading.
    let elemObj = parValidHeading.getNextSibling().asTable();
    let content = elemObj.copy();

    return content;
  }
}

/**
 * Gets the IDs of the Docs files within the folder that contains source files.
 * 
 * Called from function performImport().
 */
function getFiles(folder) {
  // Only gets Docs files.
  const files = folder.getFilesByType(MimeType.GOOGLE_DOCS);
  let docIDs = [];
  while (files.hasNext()) {
    let file = files.next();
    docIDs.push(file.getId());
  }
  return docIDs;
}

solutions/automations/aggregate-document-content/Menu.js
/**
 * Copyright 2022 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/** 
 * This file contains the functions that build the custom menu.
 */
// Menu constants for easy access to update.
const MENU = {
  NAME: 'Import summaries',
  IMPORT: 'Import summaries',
  SETUP: 'Configure',
  NEW_INSTANCE: 'Setup new instance',
  TEMPLATE: 'Create starter template',
  SAMPLES: 'Run demo setup with sample documents'
}

/**
 * Creates custom menu when the document is opened.
 */
function onOpen() {
  const ui = DocumentApp.getUi();
  ui.createMenu(MENU.NAME)
    .addItem(MENU.IMPORT, 'performImport')
    .addSeparator()
    .addSubMenu(ui.createMenu(MENU.SETUP)
      .addItem(MENU.NEW_INSTANCE, 'setupConfig')
      .addItem(MENU.TEMPLATE, 'createSampleFile')
      .addSeparator()
      .addItem(MENU.SAMPLES, 'setupWithSamples'))
    .addItem('About', 'aboutApp')
    .addToUi()
}

/**
 * About box for context and contact.
 * TODO: Personalize
 */
function aboutApp() {
  const msg = `
  ${APP_TITLE}
  Version: 1.0
  Contact: <Developer Email goes here>`

  const ui = DocumentApp.getUi();
  ui.alert("About this application", msg, ui.ButtonSet.OK);
}

Setup.gs

solutions/automations/aggregate-document-content/Setup.js
/**
 * Copyright 2022 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/** 
 * This file contains functions that create the template and sample documents.
 */

/**
 * Runs full setup configuration, with option to include samples.
 * 
 * Called from menu & setupWithSamples()
 * 
 * @param {boolean} includeSamples - Optional, if true creates samples files. * 
 */
function setupConfig(includeSamples) {

  // Gets folder to store documents in.
  const folder = getFolderByName_(PROJECT_FOLDER_NAME)

  let msg =
    `\nDrive Folder for Documents: '${PROJECT_FOLDER_NAME}'
   \nURL: \n${folder.getUrl()}`

  // Creates sample documents for testing.
  // Remove sample document creation and add your own process as needed.
  if (includeSamples) {
    let filesCreated = 0;
    for (let doc of samples.documents) {
      filesCreated += createGoogleDoc(doc, folder, true);
    }
    msg += `\n\nFiles Created: ${filesCreated}`
  }
  const ui = DocumentApp.getUi();
  ui.alert(`${APP_TITLE} [Setup]`, msg, ui.ButtonSet.OK);

}

/**
 * Creates a single document instance in the application folder.
 * Includes import settings already created [Heading | Keywords | Table]
 * 
 * Called from menu. 
 */
function createSampleFile() {

  // Creates a new Google Docs document.
  const templateName = `[Template] ${APP_TITLE}`;
  const doc = DocumentApp.create(templateName);
  const docId = doc.getId();

  const msg = `\nDocument created: '${templateName}'
  \nURL: \n${doc.getUrl()}`

  // Adds template content to the body.
  const body = doc.getBody();

  body.setText(templateName);
  body.getParagraphs()[0].setHeading(DocumentApp.ParagraphHeading.TITLE);
  body.appendParagraph('Description').setHeading(DocumentApp.ParagraphHeading.HEADING1);
  body.appendParagraph('');

  const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy');
  body.appendParagraph(`${FIND_TEXT_KEYWORDS} - ${dateString}`).setHeading(APP_STYLE);
  body.appendTable().appendTableRow().appendTableCell('TL;DR');
  body.appendParagraph("");

  // Gets folder to store documents in.
  const folder = getFolderByName_(PROJECT_FOLDER_NAME)

  // Moves document to application folder.
  DriveApp.getFileById(docId).moveTo(folder);

  const ui = DocumentApp.getUi();
  ui.alert(`${APP_TITLE} [Template]`, msg, ui.ButtonSet.OK);
}

/**
 * Configures application for demonstration by setting it up with sample documents.
 * 
 * Called from menu | Calls setupConfig with option set to true. 
 */
function setupWithSamples() {
  setupConfig(true)
}

/** 
 * Sample document names and demo content. 
 * {object} samples[]
*/
const samples = {
  'documents': [
    {
      'name': 'Project GHI',
      'description': 'Google Workspace Add-on inventory review.',
      'content': 'Reviewed all of the currently in-use and proposed Google Workspace Add-ons. Will perform an assessment on how we can reduce overlap, reduce licensing costs, and limit security exposures. \n\nNext week\'s goal is to report findings back to the Corp Ops team.'
    },
    {
      'name': 'Project DEF',
      'description': 'Improve IT networks within the main corporate building.',
      'content': 'Primarily focused on 2nd thru 5th floors in the main corporate building evaluating the network infrastructure. Benchmarking tests were performed and results are being analyzed. \n\nWill submit all findings, analysis, and recommendations next week for committee review.'
    },
    {
      'name': 'Project ABC',
      'description': 'Assess existing Google Chromebook inventory and recommend upgrades where necessary.',
      'content': 'Concluded a pilot program with the Customer Service department to perform inventory and update inventory records with Chromebook hardware, Chrome OS versions, and installed apps. \n\nScheduling a work plan and seeking necessary go-forward approvals for next week.'
    },
  ],
  'common': 'This sample document is configured to work with the Import summaries custom menu. For the import to work, the source documents used must contain a specific keyword (currently set to "Summary"). The keyword must reside in a paragraph with a set style (currently set to "Heading 3") that is directly followed by a single-cell table. The table contains the contents to be imported into the primary document.\n\nWhile those rules might seem precise, it\'s how the application programmatically determines what content is meant to be imported and what can be ignored. Once a summary has been imported, the script updates the heading font to a new color (currently set to Green, hex \'#2e7d32\') to ensure the app ignores it in future imports. You can change these settings in the Apps Script code.'
}

/**
 * Creates a sample document in application folder.
 * Includes import settings already created [Heading | Keywords | Table].
 * Inserts demo data from samples[].
 * 
 * Called from menu. 
 */
function createGoogleDoc(document, folder, duplicate) {

  // Checks for duplicates.
  if (!duplicate) {
    // Doesn't create file of same name if one already exists.
    if (folder.getFilesByName(document.name).hasNext()) {
      return 0 // File not created.
    }
  }

  // Creates a new Google Docs document.
  const doc = DocumentApp.create(document.name).setName(document.name);
  const docId = doc.getId();

  // Adds boilerplate content to the body.
  const body = doc.getBody();

  body.setText(document.name);
  body.getParagraphs()[0].setHeading(DocumentApp.ParagraphHeading.TITLE);
  body.appendParagraph("Description").setHeading(DocumentApp.ParagraphHeading.HEADING1);
  body.appendParagraph(document.description);
  body.appendParagraph("Usage Instructions").setHeading(DocumentApp.ParagraphHeading.HEADING1);
  body.appendParagraph(samples.common);

  const dateString = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'MMMM dd, yyyy');
  body.appendParagraph(`${FIND_TEXT_KEYWORDS} - ${dateString}`).setHeading(APP_STYLE);
  body.appendTable().appendTableRow().appendTableCell(document.content);
  body.appendParagraph("");

  // Moves document to application folder.
  DriveApp.getFileById(docId).moveTo(folder);

  // Returns if successfully created.
  return 1
}

Utilities.gs

solutions/automations/aggregate-document-content/Utilities.js
/**
 * Copyright 2022 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/** 
 * This file contains common utility functions.
 */

/**
 * Returns a Drive folder located in same folder that the application document is located.
 * Checks if the folder exists and returns that folder, or creates new one if not found.
 *
 * @param {string} folderName - Name of the Drive folder. 
 * @return {object} Google Drive folder
 */
function getFolderByName_(folderName) {
  // Gets the Drive folder where the current document is located.
  const docId = DocumentApp.getActiveDocument().getId();
  const parentFolder = DriveApp.getFileById(docId).getParents().next();

  // Iterates subfolders to check if folder already exists.
  const subFolders = parentFolder.getFolders();
  while (subFolders.hasNext()) {
    let folder = subFolders.next();

    // Returns the existing folder if found.
    if (folder.getName() === folderName) {
      return folder;
    }
  }
  // Creates a new folder if one doesn't already exist.
  return parentFolder.createFolder(folderName)
    .setDescription(`Created by ${APP_TITLE} application to store documents to process`);
}

/**
 * Test function to run getFolderByName_.
 * @logs details of created Google Drive folder.
 */
function test_getFolderByName() {

  // Gets the folder in Drive associated with this application.
  const folder = getFolderByName_(PROJECT_FOLDER_NAME);

  console.log(`Name: ${folder.getName()}\rID: ${folder.getId()}\rURL:${folder.getUrl()}\rDescription: ${folder.getDescription()}`)
  // Uncomment the following to automatically delete the test folder.
  // folder.setTrashed(true);
}

참여자

이 샘플은 Google에서 Google 개발자 전문가의 도움을 받아 유지관리합니다.

다음 단계