GAS API를 사용한 출석 관리 시스템

개요



리모트에 의한 이벤트등도 개최되게 되어 출결을 관리하고 싶은 일이 있다고 생각합니다. Google Form 등을 이용해도 좋지만 개인 계정을 출석에 사용하고 싶지 않은 사람도 있다고 생각합니다. 그래서 이번에는 GAS의 API를 이용해 브라우저에서 참석할 수 있는 시스템을 만들기로 했습니다.

기능 설명



웹 측



브라우저에서 액세스하여 사용합니다. 名簿 시트에 ID를 입력하여 참석할 수 있습니다. 브라우저에서 위치 및 터미널 정보를 가져와 해당 정보를 GAS API에 전달합니다.

출석 페이지



주의사항(실제로 명부 시트의 ID를 입력하여 동작을 확인하는 경우)



위치 정보에 대해 전송하지 않으려면 위치 정보 읽기 대화 상자를 허용하지 마십시오. PC 브라우저에서 ブロック를 선택하십시오.



GoogleSpreadSheet



출석 관리 스프레드시트

참석할 때 미리 만든 名簿 시트를 참조하고 出席 시트에 참석한 시간을 추가합니다.

명부 시트





출석 시트





출처



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/[email protected]/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/[email protected]/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 스프레드 시트 측 설정



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 크롬 개발자 도구Sensor에서 다른 위치 정보로 제출할 수 있습니다.



다음의 경우라면 런던의 위치 정보로 전송됩니다.



이번 경우에는 어디까지나 참고 정도로 이용하는 물건으로 보다 보안을 엄밀하게 하고 싶은 경우는, 네이티브 앱으로 작성해 인증 키등을 설정해 키 없이는 갱신할 수 없는 구조가 필요하게 될 것 같습니다.

라고는 하면서 간편하게 출석 관리를 할 수 있는 구조가 생겼으므로 간편하게 사용하는 분에는 좋을지도 모릅니다.

좋은 웹페이지 즐겨찾기