5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GASのAPIを使った出欠管理システム

Posted at

概要

リモートによるイベントなども開催されるようになり出欠を管理したいことがあると思います。Google Formなどを用いてもよいのですが私用のアカウントを出欠に使われたくない人もいると思います。そこで今回はGASのAPIを用いてブラウザから出席できるようなシステムを作ることにしました。

機能説明

ウェブ側

ブラウザからアクセスして利用します。名簿シートにあるIDを入力することで出席できます。ブラウザから位置情報と端末情報を取得してそれらの情報をGASのAPIに渡します。

[出席ページ] (https://attendant.netlify.app/)

image.png

注意事項(実際に名簿シートのIDを入力して動作を確認する場合)

位置情報について送信されたくない場合は、位置情報の読み取りダイアログを許可しないようにしてください。PCのブラウザではブロックを選択してください。

image.png

GoogleSpreadSheet

[出席管理のスプレッドシート] (https://docs.google.com/spreadsheets/d/1x9_TDyfKjsE6MevVMzsy10tdT8gop0k6fHSYlTyTDng/edit#gid=0)

出席されると事前に作成した名簿シートを参照し出席シートに出席した時刻を追記していきます。

名簿シート

image.png

出席シート

image.png

ソース

index.html

画面の表示切替についてはVue.jsを使用しております。実態はindex.htmlだけでVue.jsCDNで読み込んでいます。

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>出欠システム</title>
    <!-- <link rel="stylesheet" type="text/css" href="style.css" media="all" /> -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.8.0/css/bulma.min.css">
</head>

<body>
    <div id="app">
        <div class="hero-body">
            <div class="container has-text-centered">
                <!-- <div class="column is-4 is-offset-4"> -->
                <div class="column">
                    <h3 class="title has-text-black">出欠システム</h3>
                    <hr class="login-hr">
                    <p class="subtitle has-text-black">IDを入力して出席登録を実行してください。</p>
                    <div class="box">
                        <figure class="avatar">
                            <!-- <img src="https://placehold.it/128x128"> -->
                        </figure>
                        <form v-if="!isClicked" onsubmit="return false;">
                            <div class="field">
                                <div class="control">
                                    <input v-model="id" class="input is-large" type="text" placeholder="IDを入力してください"
                                        autofocus="">
                                </div>
                            </div>

                            <button style="margin-top: 50px;" type="button"
                                class="button is-block is-info is-large is-fullwidth" @click="attend()">出席登録 <i
                                    class="fa fa-sign-in" aria-hidden="true"></i></button>
                        </form>
                        <div v-if="isClicked" class="loader-wrapper">
                            <div class="loader is-loading"></div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios-jsonp/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script>
    new Vue({
        el: "#app",
        data: {
            id: null,
            lat: 0,
            lon: 0,
            isClicked: false,
        },
        methods: {
            attend() {
                vueThis = this
                this.isClicked = true

                let apiUrl = 'https://script.google.com/macros/s/AKfycbw9akDlm3cu309t0XwnhIqhmEGKQioBnUeo2--vpPix0aYngeU/exec'
                axios.get(`${apiUrl}?id=${this.id}&lat=${this.lat}&lon=${this.lon}`).then(function (response) {
                    console.log(response);
                    alert(response.data.msg)
                    vueThis.isClicked = false
                })
            },
        },
        mounted() {
            if (navigator.geolocation) {
                navigator.geolocation.getCurrentPosition((position) => {

                    this.lat = position.coords.latitude
                    this.lon = position.coords.longitude
                });
            }
        }
    })


</script>

<style>
    .loader-wrapper {
        position: absolute;
        top: 0;
        left: 0;
        height: 100%;
        width: 100%;
        background: #fff;
        transition: opacity .3s;
        display: flex;
        justify-content: center;
        align-items: center;
        border-radius: 6px;
        opacity: 1;
        z-index: 1;
    }

    .loader-wrapper .loader {
        height: 80px;
        width: 80px;
    }

    /* .is-active {
        opacity: 1;
        z-index: 1;
    } */
</style>

</html>

位置情報の読み取り

mountedで位置情報の機能であるnavigator.geolocationを読み込みます。最初ブラウザで開くと許可が求められ許可されればVueの変数であるlatlonに位置情報を格納します。

        mounted() {
            if (navigator.geolocation) {
                navigator.geolocation.getCurrentPosition((position) => {

                    this.lat = position.coords.latitude
                    this.lon = position.coords.longitude
                });
            }
        }

Google Action Script

Google Spread Sheet側の設定

IDの設定

spreadIdは`GoogleSpreadSheetのIDを設定します。以下のURLからIDを設定します。

https://docs.google.com/spreadsheets/d/ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX /edit#gid=597941997

d/と/editの間の「XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX」がスプレッドシートIDになります。

GASのソース

//書込先スプレッドシートのIDを入力
const spreadId = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
const sheetAttend = '出席'
const sheetMaster = '名簿'

const idCol = 1
const updateCol = 5
const execMarginMin = 30

function findRow(sheet,col,val){
  var dat = sheet.getDataRange().getValues(); //受け取ったシートのデータを二次元配列に取得
  let row = 0
  for(var i=1;i<dat.length;i++){
    if(dat[i][col-1] == val){
      // 一番下まで検索
      row = i + 1;
      //// 上から見つかったら処理中断
//      return i+1;
    }
  }
  return row;
}

function idExistCheck(id){
  let sheetMs = SpreadsheetApp.openById(spreadId).getSheetByName(sheetMaster);
  if(findRow(sheetMs,idCol,id) == 0){
    return false
  }
  return true
}

function updateCheck(id){
  let sheetAt = SpreadsheetApp.openById(spreadId).getSheetByName(sheetAttend);
  let checkRow = findRow(sheetAt,idCol,id)
  
  if(checkRow > 0){
    let date = new Date();
    let beforeUpdate = sheetAt.getRange(checkRow,updateCol).getValue()
    let diffTimeMin = (date - beforeUpdate) / (1000 * 60)
    return diffTimeMin > execMarginMin
  }
  return true
}

function test(){
//  sheet = SpreadsheetApp.openById(spreadId).getSheetByName(sheetAttend);
  
  if(idExistCheck(11111)){
    Logger.log("aaaaa")
  }else{
    Logger.log("eeeee")}
}


function doGet(e) {
  //doPost(e)にするとformからのpostデータを書き込むことが出来る
  //応用すれば、スプレッドシートをRestAPIもどきにしたり、フォームのDBにしたり、いろいろ出来ると思う
  //別名で使いたい場合の例
  //var name = e.parameter.p1;
  //var mail = e.parameter.p2;

  //JSONオブジェクト格納用の入れ物
  let resultData = {} 

  if (!e.parameter || !e.parameter.id) {
    resultData.msg = "パラメーターが不正です";
    return ContentService.createTextOutput(JSON.stringify(resultData));
  }
    
  if(!idExistCheck(e.parameter.id)){
    resultData.msg = "IDが登録されていません"
    return ContentService.createTextOutput(JSON.stringify(resultData));
  }
  
  if(!updateCheck(e.parameter.id)){
    resultData.msg = "先ほど出席されていることが確認できました。時間を空けてから実行してください。"
    return ContentService.createTextOutput(JSON.stringify(resultData));
  }
  
  let sheetAt = SpreadsheetApp.openById(spreadId).getSheetByName(sheetAttend)
  const userAgent = HtmlService.getUserAgent();
  var addArray = [ e.parameter.id , e.parameter.lat , e.parameter.lon , userAgent ,new Date() ];
  sheetAt.appendRow(addArray);
  
  resultData.msg = "出席情報を登録しました!"
  
  return ContentService.createTextOutput(JSON.stringify(resultData));

}

課題

基本的にはウェブからの情報は偽装される可能性があります。例えば位置情報の場合ではGoogle ChromeのデベロッパーツールのSensorで違う位置情報として送信することができます。

image.png

以下の場合だとロンドンの位置情報で送信されます。

image.png

今回の場合ではあくまで参考程度で利用するモノでよりセキュリティーを厳密にしたい場合は、ネイティブアプリで作成して認証キーなどを設定してキーなしでは更新できない仕組みが必要になりそうです。

とはいいつつ手軽に出席管理を行える仕組みができましたのでお手軽に使う分には良いかもしれません。

5
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?