Node.js의 빅데이터로 Google Slides 프레젠테이션 생성

1. 개요

이 Codelab에서는 가장 일반적인 소프트웨어 라이선스 분석을 위한 맞춤 프레젠테이션 도구로 Google Slides를 사용하는 방법을 알아봅니다. BigQuery API를 사용하여 GitHub의 모든 오픈소스 코드를 쿼리하고 Google Slides API를 사용하여 결과를 제시하는 슬라이드 자료를 만듭니다. 샘플 애플리케이션은 Node.js를 사용하여 빌드되었지만 동일한 기본 원칙을 모든 아키텍처에 적용할 수 있습니다.

학습할 내용

  • Slides API를 사용하여 프레젠테이션 만들기
  • BigQuery를 사용하여 대규모 데이터 세트에 대한 유용한 정보 얻기
  • Google Drive API를 사용하여 파일 복사

필요한 항목

  • Node.js가 설치됨
  • 인터넷 및 웹브라우저 액세스
  • Google 계정
  • Google Cloud Platform 프로젝트

2. 샘플 코드 가져오기

모든 샘플 코드를 컴퓨터에 다운로드할 수 있습니다.

...또는 명령줄에서 GitHub 저장소를 클론합니다.

git clone https://github.com/googleworkspace/slides-api.git

저장소에는 작동 중인 버전을 참조해야 하는 경우 프로세스의 각 단계를 나타내는 디렉터리 집합이 포함됩니다.

start 디렉터리에 있는 사본에서 작업하지만 필요에 따라 다른 항목을 참조하거나 다른 디렉터리에서 파일을 복사할 수 있습니다.

3. 샘플 앱 실행

먼저 Node 스크립트를 준비하고 실행해 보겠습니다. 코드를 다운로드한 후 아래 안내에 따라 Node.js 애플리케이션을 설치하고 시작합니다.

  1. 컴퓨터에서 명령줄 터미널을 열고 Codelab의 start 디렉터리로 이동합니다.
  2. 다음 명령어를 입력하여 Node.js 종속 항목을 설치합니다.
npm install
  1. 다음 명령어를 입력하여 스크립트를 실행합니다.
node .
  1. 이 프로젝트의 단계를 보여주는 인사말을 살펴봅니다.
-- Start generating slides. --
TODO: Get Client Secrets
TODO: Authorize
TODO: Get Data from BigQuery
TODO: Create Slides
TODO: Open Slides
-- Finished generating slides. --

slides.js, license.js, auth.js에서 TODO 목록을 확인할 수 있습니다. 각 단계는 완료 중인 이전 단계에 따라 달라지므로 Google에서는 JavaScript 프로미스를 사용하여 앱을 완료하는 데 필요한 단계를 체인합니다.

프라미스에 익숙하지 않더라도 걱정하지 마세요. 필요한 모든 코드를 제공해 드립니다. 간단히 말해 프라미스는 비동기식 처리를 보다 동기식 방식으로 처리할 수 있는 방법을 제공합니다.

4. 클라이언트 보안 비밀번호 가져오기

Slides, BigQuery, Drive API를 사용하기 위해 OAuth 클라이언트 및 서비스 계정을 만듭니다.

Google Developers Console 설정

  1. 이 마법사를 사용하여 Google Developers Console에서 프로젝트를 만들거나 선택하고 API를 자동으로 사용 설정합니다. 계속을 클릭한 후 사용자 인증 정보로 이동을 클릭합니다.
  2. 프로젝트에 사용자 인증 정보 추가 페이지에서 취소 버튼을 클릭합니다.
  3. 페이지 상단에서 OAuth 동의 화면 탭을 선택합니다. 이메일 주소를 선택하고 제품 이름 Slides API Codelab을 입력한 다음 저장 버튼을 클릭합니다.

BigQuery, Drive, Slides API 사용 설정

  1. 대시보드 탭을 선택하고 API 사용 설정 버튼을 클릭한 후 다음 3가지 API를 사용 설정합니다.
  2. BigQuery API
  3. Google 드라이브 API
  4. Google Slides API

OAuth 클라이언트 보안 비밀번호 다운로드 (Slides 및 Drive용)

  1. 사용자 인증 정보 탭을 선택하고 사용자 인증 정보 만들기 버튼을 클릭한 다음 OAuth 클라이언트 ID를 선택합니다.
  2. 애플리케이션 유형 Other를 선택하고 이름 Google Slides API Codelab을 입력한 후 만들기 버튼을 클릭합니다.확인을 클릭하여 결과 대화상자를 닫습니다.
  3. 클라이언트 ID 오른쪽에 있는 file_download (JSON 다운로드) 버튼을 클릭합니다.
  4. 보안 비밀 파일의 이름을 client_secret.json로 바꾸고 start/finish/ 디렉터리 모두에 복사합니다.

서비스 계정 비밀번호 다운로드 (BigQuery용)

  1. 사용자 인증 정보 탭을 선택하고 사용자 인증 정보 만들기 버튼을 클릭한 다음 서비스 계정 키를 선택합니다.
  2. 드롭다운에서 새 서비스 계정을 선택합니다. 서비스 이름(Slides API Codelab Service)을 선택합니다. 그런 다음 역할을 클릭하고 BigQuery로 스크롤하고 BigQuery 데이터 뷰어BigQuery 작업 사용자를 모두 선택합니다.
  3. 키 유형으로 JSON을 선택합니다.
  4. 만들기를 클릭합니다. 키 파일이 컴퓨터에 자동으로 다운로드됩니다. 닫기를 클릭하여 표시되는 대화상자를 종료합니다.
  5. 보안 비밀 파일의 이름을 service_account_secret.json로 바꾸고 start/finish/ 디렉터리 모두에 복사합니다.

클라이언트 보안 비밀번호 가져오기

start/auth.js에서 getClientSecrets 메서드를 작성해 보겠습니다.

auth.js

const fs = require('fs');

/**
 * Loads client secrets from a local file.
 * @return {Promise} A promise to return the secrets.
 */
module.exports.getClientSecrets = () => {
  return new Promise((resolve, reject) => {
    fs.readFile('client_secret.json', (err, content) => {
      if (err) return reject('Error loading client secret file: ' + err);
      console.log('loaded secrets...');
      resolve(JSON.parse(content));
    });
  });
}

이제 클라이언트 보안 비밀번호가 로드되었습니다. 사용자 인증 정보가 다음 프로미스로 전달됩니다. node .로 프로젝트를 실행하여 오류가 없는지 확인합니다.

5. OAuth2 클라이언트 만들기

슬라이드를 만들기 위해 다음 코드를 auth.js 파일에 추가하여 Google API에 인증을 추가해 보겠습니다. 이 인증을 사용하면 Google Drive에서 파일을 읽고 쓰고, Google Slides에서 프레젠테이션을 만들고, Google BigQuery에서 읽기 전용 쿼리를 실행할 수 있는 Google 계정 액세스 권한이 필요합니다. (참고: getClientSecrets는 변경하지 않았습니다.)

auth.js

const fs = require('fs');
const readline = require('readline');
const openurl = require('openurl');
const googleAuth = require('google-auth-library');
const TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
      process.env.USERPROFILE) + '/.credentials/';
const TOKEN_PATH = TOKEN_DIR + 'slides.googleapis.com-nodejs-quickstart.json';

// If modifying these scopes, delete your previously saved credentials
// at ~/.credentials/slides.googleapis.com-nodejs-quickstart.json
const SCOPES = [
  'https://www.googleapis.com/auth/presentations', // needed to create slides
  'https://www.googleapis.com/auth/drive', // read and write files
  'https://www.googleapis.com/auth/bigquery.readonly' // needed for bigquery
];

/**
 * Loads client secrets from a local file.
 * @return {Promise} A promise to return the secrets.
 */
module.exports.getClientSecrets = () => {
  return new Promise((resolve, reject) => {
    fs.readFile('client_secret.json', (err, content) => {
      if (err) return reject('Error loading client secret file: ' + err);
      console.log('loaded secrets...');
      resolve(JSON.parse(content));
    });
  });
}

/**
 * Create an OAuth2 client promise with the given credentials.
 * @param {Object} credentials The authorization client credentials.
 * @param {function} callback The callback for the authorized client.
 * @return {Promise} A promise to return the OAuth client.
 */
module.exports.authorize = (credentials) => {
  return new Promise((resolve, reject) => {
    console.log('authorizing...');
    const clientSecret = credentials.installed.client_secret;
    const clientId = credentials.installed.client_id;
    const redirectUrl = credentials.installed.redirect_uris[0];
    const auth = new googleAuth();
    const oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);

    // Check if we have previously stored a token.
    fs.readFile(TOKEN_PATH, (err, token) => {
      if (err) {
        getNewToken(oauth2Client).then(() => {
          resolve(oauth2Client);
        });
      } else {
        oauth2Client.credentials = JSON.parse(token);
        resolve(oauth2Client);
      }
    });
  });
}

/**
 * Get and store new token after prompting for user authorization, and then
 * fulfills the promise. Modifies the `oauth2Client` object.
 * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
 * @return {Promise} A promise to modify the oauth2Client credentials.
 */
function getNewToken(oauth2Client) {
  console.log('getting new auth token...');
  openurl.open(oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES
  }));

  console.log(''); // \n
  return new Promise((resolve, reject) => {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout
    });
    rl.question('Enter the code from that page here: ', (code) => {
      rl.close();
      oauth2Client.getToken(code, (err, token) => {
        if (err) return reject(err);
        oauth2Client.credentials = token;
        let storeTokenErr = storeToken(token);
        if (storeTokenErr) return reject(storeTokenErr);
        resolve();
      });
    });
  });
}

/**
 * Store token to disk be used in later program executions.
 * @param {Object} token The token to store to disk.
 * @return {Error?} Returns an error or undefined if there is no error.
 */
function storeToken(token) {
  try {
    fs.mkdirSync(TOKEN_DIR);
    fs.writeFileSync(TOKEN_PATH, JSON.stringify(token));
  } catch (err) {
    if (err.code != 'EEXIST') return err;
  }
  console.log('Token stored to ' + TOKEN_PATH);
}

6. BigQuery 설정

BigQuery 살펴보기 (선택사항)

BigQuery를 사용하면 몇 초 만에 방대한 데이터 세트를 쿼리할 수 있습니다. 프로그래매틱 방식으로 쿼리하기 전에 웹 인터페이스를 사용해 보겠습니다. 이전에 BigQuery를 설정한 적이 없다면 이 빠른 시작의 단계를 따르세요.

Cloud 콘솔을 열어 BigQuery에서 제공되는 GitHub 데이터를 둘러보고 자체 쿼리를 실행합니다. 이 쿼리를 작성하고 실행 버튼을 눌러 GitHub에서 가장 많이 사용되는 소프트웨어 라이선스를 알아보겠습니다.

bigquery.sql

WITH AllLicenses AS (
  SELECT * FROM `bigquery-public-data.github_repos.licenses`
)
SELECT
  license,
  COUNT(*) AS count,
  ROUND((COUNT(*) / (SELECT COUNT(*) FROM AllLicenses)) * 100, 2) AS percent
FROM `bigquery-public-data.github_repos.licenses`
GROUP BY license
ORDER BY count DESC
LIMIT 10

방금 GitHub에서 수백만 개의 공개 저장소를 분석한 결과 가장 인기 있는 라이선스를 발견했습니다. 아주 잘 분석하죠! 이번에는 동일한 쿼리를 프로그래매틱 방식으로 실행하도록 설정해 보겠습니다.

BigQuery 설정

license.js 파일의 코드를 바꿉니다. bigquery.query 함수는 프로미스를 반환합니다.

license**.js**

const google = require('googleapis');
const read = require('read-file');
const BigQuery = require('@google-cloud/bigquery');
const bigquery = BigQuery({
  credentials: require('./service_account_secret.json')
});

// See codelab for other queries.
const query = `
WITH AllLicenses AS (
  SELECT * FROM \`bigquery-public-data.github_repos.licenses\`
)
SELECT
  license,
  COUNT(*) AS count,
  ROUND((COUNT(*) / (SELECT COUNT(*) FROM AllLicenses)) * 100, 2) AS percent
FROM \`bigquery-public-data.github_repos.licenses\`
GROUP BY license
ORDER BY count DESC
LIMIT 10
`;

/**
 * Get the license data from BigQuery and our license data.
 * @return {Promise} A promise to return an object of licenses keyed by name.
 */
module.exports.getLicenseData = (auth) => {
  console.log('querying BigQuery...');
  return bigquery.query({
    query,
    useLegacySql: false,
    useQueryCache: true,
  }).then(bqData => Promise.all(bqData[0].map(getLicenseText)))
    .then(licenseData => new Promise((resolve, reject) => {
      resolve([auth, licenseData]);
    }))
    .catch((err) => console.error('BigQuery error:', err));
}

/**
 * Gets a promise to get the license text about a license
 * @param {object} licenseDatum An object with the license's
 *   `license`, `count`, and `percent`
 * @return {Promise} A promise to return license data with license text.
 */
function getLicenseText(licenseDatum) {
  const licenseName = licenseDatum.license;
  return new Promise((resolve, reject) => {
    read(`licenses/${licenseName}.txt`, 'utf8', (err, buffer) => {
      if (err) return reject(err);
      resolve({
        licenseName,
        count: licenseDatum.count,
        percent: licenseDatum.percent,
        license: buffer.substring(0, 1200) // first 1200 characters
      });
    });
  });
}

프로미스의 콜백 내부에 있는 일부 데이터를 console.log하여 객체의 구조를 이해하고 코드가 작동하는 것을 확인합니다.

7. 슬라이드 만들기

이제 재미있는 부분을 할 차례입니다. Slides API의 createbatchUpdate 메서드를 호출하여 슬라이드를 만들어 보겠습니다. 파일은 다음과 같이 바꿉니다.

slides.js

const google = require('googleapis');
const slides = google.slides('v1');
const drive = google.drive('v3');
const openurl = require('openurl');
const commaNumber = require('comma-number');

const SLIDE_TITLE_TEXT = 'Open Source Licenses Analysis';

/**
 * Get a single slide json request
 * @param {object} licenseData data about the license
 * @param {object} index the slide index
 * @return {object} The json for the Slides API
 * @example licenseData: {
 *            "licenseName": "mit",
 *            "percent": "12.5",
 *            "count": "1667029"
 *            license:"<body>"
 *          }
 * @example index: 3
 */
function createSlideJSON(licenseData, index) {
  // Then update the slides.
  const ID_TITLE_SLIDE = 'id_title_slide';
  const ID_TITLE_SLIDE_TITLE = 'id_title_slide_title';
  const ID_TITLE_SLIDE_BODY = 'id_title_slide_body';

  return [{
    // Creates a "TITLE_AND_BODY" slide with objectId references
    createSlide: {
      objectId: `${ID_TITLE_SLIDE}_${index}`,
      slideLayoutReference: {
        predefinedLayout: 'TITLE_AND_BODY'
      },
      placeholderIdMappings: [{
        layoutPlaceholder: {
          type: 'TITLE'
        },
        objectId: `${ID_TITLE_SLIDE_TITLE}_${index}`
      }, {
        layoutPlaceholder: {
          type: 'BODY'
        },
        objectId: `${ID_TITLE_SLIDE_BODY}_${index}`
      }]
    }
  }, {
    // Inserts the license name, percent, and count in the title
    insertText: {
      objectId: `${ID_TITLE_SLIDE_TITLE}_${index}`,
      text: `#${index + 1} ${licenseData.licenseName}  — ~${licenseData.percent}% (${commaNumber(licenseData.count)} repos)`
    }
  }, {
    // Inserts the license in the text body paragraph
    insertText: {
      objectId: `${ID_TITLE_SLIDE_BODY}_${index}`,
      text: licenseData.license
    }
  }, {
    // Formats the slide paragraph's font
    updateParagraphStyle: {
      objectId: `${ID_TITLE_SLIDE_BODY}_${index}`,
      fields: '*',
      style: {
        lineSpacing: 10,
        spaceAbove: {magnitude: 0, unit: 'PT'},
        spaceBelow: {magnitude: 0, unit: 'PT'},
      }
    }
  }, {
    // Formats the slide text style
    updateTextStyle: {
      objectId: `${ID_TITLE_SLIDE_BODY}_${index}`,
      style: {
        bold: true,
        italic: true,
        fontSize: {
          magnitude: 10,
          unit: 'PT'
        }
      },
      fields: '*',
    }
  }];
}

/**
 * Creates slides for our presentation.
 * @param {authAndGHData} An array with our Auth object and the GitHub data.
 * @return {Promise} A promise to return a new presentation.
 * @see https://developers.google.com/apis-explorer/#p/slides/v1/
 */
module.exports.createSlides = (authAndGHData) => new Promise((resolve, reject) => {
  console.log('creating slides...');
  const [auth, ghData] = authAndGHData;

  // First copy the template slide from drive.
  drive.files.copy({
    auth: auth,
    fileId: '1toV2zL0PrXJOfFJU-NYDKbPx9W0C4I-I8iT85TS0fik',
    fields: 'id,name,webViewLink',
    resource: {
      name: SLIDE_TITLE_TEXT
    }
  }, (err, presentation) => {
    if (err) return reject(err);

    const allSlides = ghData.map((data, index) => createSlideJSON(data, index));
    slideRequests = [].concat.apply([], allSlides); // flatten the slide requests
    slideRequests.push({
      replaceAllText: {
        replaceText: SLIDE_TITLE_TEXT,
        containsText: { text: '{{TITLE}}' }
      }
    })

    // Execute the requests
    slides.presentations.batchUpdate({
      auth: auth,
      presentationId: presentation.id,
      resource: {
        requests: slideRequests
      }
    }, (err, res) => {
      if (err) {
        reject(err);
      } else {
        resolve(presentation);
      }
    });
  });
});

8. Slides 열기

마지막으로 브라우저에서 프레젠테이션을 열어보겠습니다. slides.js에서 다음 메서드를 업데이트합니다.

slides.js

/**
 * Opens a presentation in a browser.
 * @param {String} presentation The presentation object.
 */
module.exports.openSlidesInBrowser = (presentation) => {
  console.log('Presentation URL:', presentation.webViewLink);
  openurl.open(presentation.webViewLink);
}

프로젝트를 마지막으로 한 번 실행하여 최종 결과를 표시합니다.

9. 축하합니다.

BigQuery를 사용하여 분석된 데이터로 Google Slides를 생성했습니다. 스크립트는 Google Slides API 및 BigQuery를 사용하여 가장 일반적인 소프트웨어 라이선스에 대한 분석을 보고하는 프레젠테이션을 만듭니다.

가능한 개선사항

다음은 더욱 효과적인 통합을 위한 몇 가지 추가 아이디어입니다.

  • 각 슬라이드에 이미지 추가
  • Gmail API를 사용하여 이메일을 통해 슬라이드 공유하기
  • 템플릿 슬라이드를 명령줄 인수로 맞춤설정

자세히 알아보기