Skip to main content

GAS Interface

REST API#

  • 외부 시스템에서 G.Suite에 REST방식으로 접근할 수 있도록 API 제공
  • For javascript: https://apis.google.com/js/api.js
  • 문서 파일 및 스크립트 생성및 편집 기능
  • 스크립트 함수 호출 기능

G.Suite와 외부 Interface 방법#

  • 외부 시스템에서 직접적으로 문서파일 등에 접근 가능 (2계층 모델)
  • 직접적인 문서의 접근은 GAS로 구현하고 외부에서 함수 호출하는 MVC 방식 (3계층 모델)
  • 두번째 방식을 채용

Project 구현및 Debugging#

Project 생성#

  • Drive New - More - Google Apps Script 메뉴 선택
  • Google Apps Script가 메뉴에 없으면 Connect more apps에서 Google Apps Script 검색하여 연결후 작업
  • 생성후 브라우저에 Script Editor가 표시

Project 이름 설정#

  • Editor에서 Untitled Project를 적절한 이름으로 변경 후 저장
  • 외부에서는 자동생성된 Script Id를 기준으로 접근하므로 이름은 관리상의 목적으로 사용

외부 연동 함수 구현#

  • G.Suite에 연동할때 DocumentApp, SpreadsheetApp, FormApp, MailApp등의 전용 Class를 사용

함수 Debugging#

  • Editor 상단에서 함수 선택 후 Run 또는 Debug
  • 인자가 있는 함수일 경우 테스트 함수를 작성해서 테스트
  • 코드중에 Logger.log 함수로 로그를 남기면 View - Logs 에서 확인

Project Publish#

스크립트를 외부에서 접근할 수 있도록 하려면 Publish및 보안 설정 과정 필요

Deploy#

  • Editor의 Publish - Deploy as API Executable 메뉴
  • 버전정보를 입력 후 Deploy

Cloud Platform 등록#

  • Editor의 Resources - Cloud Platform project 메뉴
  • 상단의 Project Name - project-id-xxxx 링크를 클릭하면 Cloud Platform에 해당 id로 생성후 이동

::: warning 2019. 4. 19 추가 GCP가 Update되면서 Editor에서 자동 생성되는 기능이 차단됨. GCP에서 수동으로 Project를 생성 후 Auth Consent screen 설정 후 Resources - Cloud Platform project에서 생성된 프로젝트의 번호를 등록해주어야 함. :::

Project 보안 설정#

OAuth Consent screen 설정#

  • API overview - Credentials 페이지로 이동
  • OAuth Consent screen 탭으로 이동
  • Authorized domains section에 외부에서 접근할 페이지의 도메인을 지정(localhost 제외)

::: warning 주의 사항 등록한지 얼마 안된 도메인은 인식이 안될 수 있다. DNS 캐시가 갱신될 때 까지 기다려야한다. (2 ~ 48 시간) :::

Credentials Client 설정#

  • Credentials 탭으로 이동
  • Cloud Platform으로 Project생성시 App Scripts라는 client ID 자동 생성. Script Editor에서 사용하는 Client로 삭제 시 디버깅및 실행 오류 해당 Client는 사용자에 편집권한이 없으므로 사용 불가 외부에서 사용할 별도 Client 필요
  • create credentials - OAuth client ID 선택
  • Authorized JavaScript origins 항목에 접근할 수 있는 Root URI를 등록 예) http://localhost:8080, https://mywebsvr.com
  • 생성후 발급된 Client ID를 기록

Credentials API Key 설정#

  • create credentials - API Key 선택
  • Restrict는 설정 필요하지 않음
  • 발급된 API Key를 기록

Timer Trigger 설정#

  • Editor의 Edit - Current Project's Tirgger 선택
  • 트리거 추가
  • 실행 함수 선택하고 발생 주기를 선택하고 저장
  • Trigger화면에서 Executions, Failed Executions 로그 조회 가능

Script 호출#

필수 정보#

client load#

gapi.load('client:auth2', callback);

sign in#

gapi.auth2.getAuthInstance().signIn({
scope: SCOPE,
});

init#

gapi.client
.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: ['https://script.googleapis.com/$discovery/rest?version=v1'],
scope: SCOPES,
})
.then(
function () {
// success code
},
function (error) {
// fail code
}
);

function call#

gapi.client.script.scripts
.run({
scriptId: SCRIPT_ID,
resource: {
function: 'functionName',
parameters: [parameter1, parameter2, parameter3],
},
})
.then(function (result) {
// success code
})
.catch((error) => {
// fail code
});

::: warning 추후 개선 Point 사용자 인증을 코드로 자동 처리해야 하는데, 현재까진 해당부분 학습이 안되어 수동으로 인증해야 합니다.:::

에제 프로젝트 개요#

  • Spreadsheet을 data repository로 활용
  • 라이선스 요청, 승인 과정 외부 웹화면 연동및 메일발송
  • Timer Trigger로 발급예정일 도래시 공지 발송

예제 프로젝트 소스#

Google Apps Script#

  • Code.gs
// 테스트 함수
function myFunction() {
addRequest('jun', 'jungjun88@gmail.com', 'mywebsvr1.com');
}
var MANAGER_EMAIL = 'jun@realgrid.com';
var SHEET_FILE_ID = '1IrT2HVwjg-7T8qkfjOvnWiOXAT94_GUFd_G5TYHC66E'; // file id
var SHEET_NAME = 'evaluation'; // sheet name
// sheet column names
var COL_REQ_ID = 'requestId';
var COL_USER_ID = 'userId';
var COL_EMAIL = 'email';
var COL_DOMAIN = 'domain';
var COL_REQ_DATE = 'requestDate';
var COL_ISU_DATE = 'issueDate';
var COL_LICENSE = 'license';
var COL_EXP_DATE = 'expireDate';
var COL_NOTICED = 'expireNoticed';
// request email에 보낼 승인 url 도메인
var SERVER_URL = 'http://mywebsvr.ml/';
function sendRequestEmail(reqId, userId, email, domain) {
try {
var issueUrl = SERVER_URL + 'issue.html?requestId=' + reqId;
Logger.log(issueUrl);
var mailMessage =
'<html><body>' +
'<p><h2>다음과 같이 라이선스가 신청되었습니다.ttt</h2></p>' +
'사용자: ' +
userId +
'<br/>' +
'이메일: ' +
email +
'<br/>' +
'도메인: ' +
domain +
'<br/>' +
'<a href="' +
issueUrl +
'">발급 처리</a>' +
'url = ' +
issueUrl +
'</body></html>';
MailApp.sendEmail(MANAGER_EMAIL, '라이선스 신청', '', {
htmlBody: mailMessage,
});
} catch (e) {
throw new Error('신청 메일 발송 실패, ' + e);
}
}
function sendLicenseNotify(userId, email, domain, expireDate) {
try {
var expire = Utilities.formatDate(expireDate, 'KST', 'yyyy.MM.dd');
var mailMessage =
'<html><body>' +
'고객(' +
userId +
')님의 ' +
domain +
'도메인(' +
domain +
')의 라이선스가' +
expire +
'일에 만료됩니다.<br/>' +
'라이선스 <a href="" target="_blank">재발급</a>하여 사용하시기 바랍니다' +
'</body></html>';
MailApp.sendEmail(email, '라이선스 만료 알림', '', {
htmlBody: mailMessage,
});
} catch (e) {
throw new Error('만료 알림 메일 발송 실패, ' + e);
}
}
function makeUniqueId(sheetData, reqCol) {
var date = new Date();
var id = 'R' + new Date().toISOString().slice(0, 10).replace(/-/g, '') + '-';
var lastNo = 0;
if (sheetData.length > 1) {
var rowId = sheetData[sheetData.length - 1][reqCol];
if (rowId.indexOf(id) == 0) {
lastNo = parseInt(rowId.substring(10, 13));
}
}
return id + Utilities.formatString('%03d', lastNo + 1);
}
function makeLicenseKey(domain, expireDate) {
var expire = Utilities.formatDate(expireDate, 'KST', 'yyyy.MM.dd');
return blowfish.encrypt(
'name=RealGridJs;lic=Evaluation;server=' +
domain +
';expire=' +
expire +
';vender=',
'dkdlemf2'
);
}
function addRequest(userId, email, domain) {
var sheet = SpreadsheetApp.openById(SHEET_FILE_ID).getSheetByName(SHEET_NAME);
var table = sheet.getDataRange().getValues();
var header = table[0];
var colReqId = header.indexOf(COL_REQ_ID);
var colUserId = header.indexOf(COL_USER_ID);
var colEmail = header.indexOf(COL_EMAIL);
var colDomain = header.indexOf(COL_DOMAIN);
var colReqDate = header.indexOf(COL_REQ_DATE);
var colIssueDate = header.indexOf(COL_ISU_DATE);
var colLicense = header.indexOf(COL_LICENSE);
var colExpDate = header.indexOf(COL_EXP_DATE);
var row = [];
var reqId = makeUniqueId(table, colReqId);
row[colReqId] = reqId;
row[colUserId] = userId;
row[colEmail] = email;
row[colDomain] = domain;
row[colReqDate] = new Date();
sheet.appendRow(row);
sendRequestEmail(reqId, userId, email, domain);
}
function issueLicense(reqId) {
var sheet = SpreadsheetApp.openById(SHEET_FILE_ID).getSheetByName(SHEET_NAME);
var table = sheet.getDataRange().getValues();
var header = table[0];
var colReqId = header.indexOf(COL_REQ_ID);
var colDomain = header.indexOf(COL_DOMAIN);
var colIssueDate = header.indexOf(COL_ISU_DATE);
var colLicense = header.indexOf(COL_LICENSE);
var colExpDate = header.indexOf(COL_EXP_DATE);
for (var r = table.length - 1; r > 0; r--) {
if (table[r][colReqId] == reqId) {
if (!table[r][colIssueDate]) {
var issueDate = new Date();
var domain = sheet.getRange(r + 1, colDomain + 1).getValue();
sheet.getRange(r + 1, colIssueDate + 1).setValue(issueDate);
issueDate.setMonth(issueDate.getMonth() + 1);
sheet.getRange(r + 1, colExpDate + 1).setValue(issueDate);
sheet
.getRange(r + 1, colLicense + 1)
.setValue(makeLicenseKey(domain, issueDate));
}
break;
}
}
}
function getDiffDays(from, to) {
return Math.round((to.getTime() - from.getTime()) / 1000 / 3600 / 24);
}
// Timer Trigger에서 호출하는 함수
function batchLicenseExpire() {
var sheet = SpreadsheetApp.openById(SHEET_FILE_ID).getSheetByName(SHEET_NAME);
var table = sheet.getDataRange().getValues();
var header = table[0];
var colUserId = header.indexOf(COL_USER_ID);
var colEmail = header.indexOf(COL_EMAIL);
var colDomain = header.indexOf(COL_DOMAIN);
var colExpDate = header.indexOf(COL_EXP_DATE);
var colNoticed = header.indexOf(COL_NOTICED);
var currDate = new Date();
for (var r = table.length - 1; r > 0; r--) {
var row = table[r];
if (row[colExpDate] && row[colNoticed] != 'Y') {
var diff = getDiffDays(currDate, row[colExpDate]);
if (diff < 7) {
sendLicenseNotify(
row[colUserId],
row[colEmail],
row[colDomain],
row[colExpDate]
);
sheet.getRange(r + 1, colNoticed + 1).setValue('Y');
}
}
}
}

Client#

  • consts.js
var CLIENT_ID =
'1015626781638-q467n298keb4gs5kkbd36ettc57epr1r.apps.googleusercontent.com';
var API_KEY = 'AIzaSyAQ2LPnk_XdZhcDmKl0UH3RtkIkiyykhWg';
var DISCOVERY_DOCS = [
'https://script.googleapis.com/$discovery/rest?version=v1',
];
var SCOPES =
'https://www.googleapis.com/auth/script.send_mail https://www.googleapis.com/auth/spreadsheets';
var SCRIPT_ID = '1dSoq6kx63QYE4UrCTVGnn5bmSCwKpUgGILNlZx0BColREXMxPH4BFkq7';
  • request.html
<html>
<!DOCTYPE html>
<head>
<title>RealGrid License Request</title>
<meta charset="utf-8" />
</head>
<body>
<div id="requestForm" style='visibility: hidden'>
<p>License Request</p>
<table>
<tr>
<td>UserId</td>
<td><input id="txtUserId"/></td>
</tr>
<tr>
<td>Email</td>
<td><input id="txtEmail"/></td>
</tr>
<tr>
<td>Domain</td>
<td><input id="txtDomain"/></td>
</tr>
<tr>
<td colspan="2" style="text-align: right;">
<button id="btnSubmit">Submit</button>
</td>
</tr>
</table>
</div>
<pre id="content" style="white-space: pre-wrap;"></pre>
<script type="text/javascript" src="scripts/gas_consts.js"></script>
<script type="text/javascript">
var submitButton = document.getElementById('btnSubmit');
submitButton.onclick = requestSubmit;
function clientReady() {
gapi.load('client:auth2', initClient);
}
function appendPre(message) {
var pre = document.getElementById('content');
var textContent = document.createTextNode(message + '\n');
pre.appendChild(textContent);
}
function showForm(visible) {
var form = document.getElementById('requestForm');
form.style.visibility = visible ? 'visible' : 'hidden';
}
function requestSubmit() {
var userId = document.getElementById('txtUserId').value;
var email = document.getElementById('txtEmail').value;
var domain = document.getElementById('txtDomain').value;
callAddRequest(userId, email, domain);
}
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
}, function(error) {
appendPre(JSON.stringify(error, null, 2));
});
}
function updateSigninStatus(isSignedIn) {
if (isSignedIn) {
showForm(true);
} else {
showForm(false);
}
}
function callAddRequest(userId, email, domain) {
gapi.client.script.scripts.run({
'scriptId': SCRIPT_ID,
'resource': {
'function': 'addRequest',
'parameters': [userId, email, domain]
}
}).then(function(result) {
if (result.error && result.error.status) {
appendPre('Error calling API:');
appendPre(JSON.stringify(result, null, 2));
} else if (result.error) {
var error = result.error.details[0];
appendPre('Script error message: ' + error.errorMessage);
if (error.scriptStackTraceElements) {
appendPre('Script error stacktrace:');
for (var i = 0; i < error.scriptStackTraceElements.length; i++) {
var trace = error.scriptStackTraceElements[i];
appendPre('\t' + trace.function + ':' + trace.lineNumber);
}
}
} else {
appendPre('request succeed.');
}
}).catch((error) => {
appendPre('Error calling API:');
var body = JSON.parse(error.body);
appendPre(body.error.message);
});
}
</script>
<script async defer src="https://apis.google.com/js/api.js"
onload="this.onload=function(){};clientReady();"
onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
</body>
</html>
  • issue.html
<html>
<!DOCTYPE html>
<head>
<title>RealGrid License Issue</title>
<meta charset="utf-8" />
</head>
<body>
<div id="parameterMissingMessage" style='display: none'>
<p><h3>요청 ID 파라메터가 누락되었습니다.</h3></p>
<p><button onclick='window.close();'>닫기</button>
</div>
<div id="completeMessage" style='display: none'>
<p><h3>라이선스가 발급되었습니다.</h3></p>
<p><button onclick='window.close();'>닫기</button>
</div>
<pre id="content" style="white-space: pre-wrap;"></pre>
<script type="text/javascript" src="scripts/gas_consts.js"></script>
<script type="text/javascript">
var requestId;
function clientReady() {
requestId = getParameterByName('requestId');
if (requestId) {
gapi.load('client:auth2', initClient);
} else {
showParameterMissing();
}
}
function appendPre(message) {
var pre = document.getElementById('content');
var textContent = document.createTextNode(message + '\n');
pre.appendChild(textContent);
}
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, '\\$&');
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
function showCompleted() {
document.getElementById('completeMessage').style.display = 'block';
}
function showParameterMissing() {
document.getElementById('parameterMissingMessage').style.display = 'block';
}
function initClient() {
gapi.client.init({
apiKey: API_KEY,
clientId: CLIENT_ID,
discoveryDocs: DISCOVERY_DOCS,
scope: SCOPES
}).then(function () {
gapi.auth2.getAuthInstance().isSignedIn.listen(updateSigninStatus);
updateSigninStatus(gapi.auth2.getAuthInstance().isSignedIn.get());
}, function(error) {
appendPre(JSON.stringify(error, null, 2));
});
}
function updateSigninStatus(isSignedIn) {
if (isSignedIn) {
callIssueLicense(requestId);
}
}
function callIssueLicense(requestId) {
gapi.client.script.scripts.run({
'scriptId': SCRIPT_ID,
'resource': {
'function': 'issueLicense',
'parameters': [requestId]
}
}).then(function(result) {
if (result.error && result.error.status) {
appendPre('Error calling API:');
appendPre(JSON.stringify(result, null, 2));
} else if (result.error) {
var error = result.error.details[0];
appendPre('Script error message: ' + error.errorMessage);
if (error.scriptStackTraceElements) {
appendPre('Script error stacktrace:');
for (var i = 0; i < error.scriptStackTraceElements.length; i++) {
var trace = error.scriptStackTraceElements[i];
appendPre('\t' + trace.function + ':' + trace.lineNumber);
}
}
} else {
showCompleted();
}
}).catch((error) => {
appendPre('Error calling API:');
var body = JSON.parse(error.body);
appendPre(body.error.message);
});
}
</script>
<script async defer src="https://apis.google.com/js/api.js"
onload="this.onload=function(){};clientReady();"
onreadystatechange="if (this.readyState === 'complete') this.onload()">
</script>
</body>
</html>