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

1. 개요

이 Codelab에서는 가장 일반적인 소프트웨어 라이선스 분석을 위한 맞춤 프레젠테이션 도구로 Google 슬라이드를 사용하는 방법을 알아봅니다. 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에서 할 일 목록을 확인할 수 있습니다. 각 단계는 이전 단계가 완료되어야 하므로 앱을 완료하는 데 필요한 단계를 연결하기 위해 JavaScript Promises를 사용합니다.

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

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

Slides, Bigquery, Drive API를 사용하려면 OAuth 클라이언트와 서비스 계정을 만들어야 합니다.

Google Developers Console 설정

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

BigQuery, Drive, Slides API 사용 설정

  1. 대시보드 탭을 선택하고 API 사용 설정 버튼을 클릭하여 다음 3개의 API를 사용 설정합니다.
  2. BigQuery API
  3. Google Drive API
  4. Google Slides API

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

  1. 사용자 인증 정보 탭을 선택하고 사용자 인증 정보 만들기 버튼을 클릭한 후 OAuth 클라이언트 ID를 선택합니다.
  2. 애플리케이션 유형 기타를 선택하고 이름 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 드라이브에서 파일을 읽고 쓰고, 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
      });
    });
  });
}

Promise 콜백 내부의 데이터를 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 슬라이드를 생성했습니다. 스크립트는 Google Slides API와 BigQuery를 사용하여 가장 일반적인 소프트웨어 라이선스 분석을 보고하는 프레젠테이션을 만듭니다.

가능한 개선사항

더욱 매력적인 통합을 만들기 위한 몇 가지 추가 아이디어는 다음과 같습니다.

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

자세히 알아보기