🎓

GASとReact+Viteで作る新入生受付システム

2024/02/07に公開

はじめに

大学の生協っぽいところでアルバイトをしているろくまいると申します。
私の大学では、毎年4月にPCガイダンス(おそらくどの大学もある)というものがあり、大勢の大学1年生をPCの種別(大学推奨機 or 非推奨機)に応じて受付を行っています。
この受付を捌くことができる、敏腕パートさんが作ったAccess製の新入生受付システムがありましたが、プロジェクト自体が属人化しており、パートさんの退職に伴って誰も管理できない状態になってしまいました。
受付業務の効率化においてもシステムは必要不可欠なので、新たな受付システムをきちんと保守できる体制の上で作り直すことにしました。

完成品は以下の画像です。
これを作った流れを書きつつ、苦戦した部分についても触れていきます。

要件定義

まず、実際に受付をする方々の要望を聞き出し、どの程度機能に落とし込むのかを考えました。
もちろん、今まで動いていたシステムを完全に再現できれば文句ないのですが、それでは開発期間の長期化やその機能を再現するのに用いたライブラリのメンテナンス終了などによって保守段階で痛い目を見ることになる可能性があります。
保守を容易にするためにも、複雑になりそうな機能実装は行わないことを決めました。
それらを先方にも納得していただいた上で、ざっと要求を洗い出した結果以下のような感じになりました。

重要度 機能概要 要求
受付登録・取消 正常に受付・取消処理ができる
認証 受付処理に従事する人のみがアクセスできる
受付モードの切り替え PCの種別毎に受付ができること
備考欄の編集 案内時に特記事項が生じた場合、情報共有できること

この際に大切だと感じたのは、「〜になったら(であれば)、〇〇してほしい」などの例外処理をどうするか明確にしておくことです。
業務アプリケーション特有の配慮すべき事項は、@ForestMountain1234さんの「工場内で2年間業務アプリ開発をして分かった事」という記事がとても参考になりました…(読んだのは開発終盤だった)

開発フェーズでも何度か「こういうときにこうしてほしい」といった変更が飛んでくることがあり、開発を円滑に進めるためにも、事前のすり合わせが重要であることを痛感させられました。

これらの他に、非機能要件である保守マニュアル・導入ガイドなども含めて提供する形で合意しました。

技術選定

今回、学生情報を引っ張ってきて表示する上に受付状態というステートを持たなくてはいけない案件であるため、DBを用意することが必要不可欠です。従来ではAccessがその役割を担っていましたが、誰も保守できないので使えません。
しかし、幸いなことに元々業務改善を学生主体で行える環境ではあったので、GASを使った社内(?)ツールがいくつかありました。(例えばGoogleフォームと連携して、フォーム入力があったらメールを飛ばして、Slackに通知するなど)
また、認証が今回必要ですが、Spreadsheetのアクセス制限のみ考えれば良いです。
(後に紹介するレシート印刷Webページからは、学生情報にアクセスできないため)
これらを考慮した結果、今回もGoogle様におんぶに抱っこでGAS(+Spreadsheet)をベースに開発を進めていくことになりました。
バックエンドはこれで解決しましたが、フロントエンドをどうするか悩みました。

誰でも保守できるという観点で言えば、HTML+CSS+JSで書くのがベストですが、扱う情報が多く@jirokunさんが「普通のJavaScriptで陥りがちなこと」という記事で述べられているセレクタ地獄イベント地獄に陥ることが目に見えています。

別の業務でReactを触っており、Component-Basedの素晴らしさを感じている身からすると、このプロジェクトで生JSなんて地獄以外の何物でもありません。きちんと別の人に引き継ぐことを約束の上、Reactを使わせていただくことにしました。

その他、Viteに関しても、高速ビルド・ホットリロードができる優れものであるということで採用しましたが、同様の事例で真下(@mashita1023)さんの「GAS + React + Vite + Claspで作るお手軽フロントエンド」という記事を見つけ、vite-plugin-singlefileを使うと、単一HTMLで吐き出せるという情報を得ることができたのもあって選びました。

デザインについては、業務Webアプリケーションでよく使われている(と個人的に感じている)Bootstrapを使用しました。

開発

いよいよ開発フェーズに移ります。大きく2つの観点で説明します。

フロント・バックエンド

GASの標準エディタはクソ使いづらいと思っており、更にgetValue()でスプレッドシートから取得してきた値の型を厳密に管理したかったので、claspを使いました。特に、TypeScriptを使っている場合は恩恵が大きく、GASにpushする際にJavaScriptへコンパイルしたものを上げてくれるので非常に助かりました。おすすめです。
基本的には、バックエンド側(GASの関数郡)から開発を進め、それぞれに対して簡易テスト(TSの静的解析及び単体テスト)を行い、その後にフロントエンド側の制作を行いました。フロントエンド側は今回時間がなく、バックエンド->フロントエンドという流れであったので特にStorybookなどを活用したUIテストは行いませんでした。その代わり、結合テストでは十分時間を取り、「想定外な値が与えられた際はどうなるか」などの例外処理の部分を確認しました。

フロントの表示は利便性重視でメニューにボタンを追加して、showModalDialog()を用いてページをモーダル表示する形にしました。

// メニューに起動ボタンを追加
function onOpen(): void {
  SpreadsheetApp.getUi()
    .createMenu("新入生受付システム")
    .addItem("起動", "showModal")
    .addToUi();
}

// モーダルダイアログを表示
function showModal(): void {
  const container: GoogleAppsScript.HTML.HtmlOutput = HtmlService.createHtmlOutputFromFile("webpanel/index.html").setWidth(1300).setHeight(1000);
  SpreadsheetApp.getUi().showModalDialog(container, "新入生受付システム");
}

レシートの印刷

こちらは苦戦しました。Webアプリケーションからレシートを印刷するということがメジャーでないためか、なかなか情報がなかったです。唯一見つけた@yamukotonakuさんが書かれている「ReceiptLine を使って Web アプリケーションからレシート・ラベルシールを印刷する」という記事のやり方が良いと思いました。
しかし、この方法だとプリントサーバーを作らなくてはならず、なかなか大変なシステム構成になり、保守性が悪くなるため採用を見送りました。

本当はレシート記述言語(ReceiptLine)を使って実装したかったのですが、今まで使用していたサーマルプリンタ(​和信テック株式会社さんのWS-R330H)で何故か日本語が文字化けしてしまったりと問題が多々あったので、あえなく断念しました…

代わりに、GASのdoGET関数を使ってレシート印刷用Webページを表示し、ブラウザの標準印刷機能でプリントを行う手法へ切り替えました。

仕組みはいたって簡単で、クエリパラメータに学籍番号と氏名と受付日時を乗せて印刷用ページを開いているだけです。

function doGet(e): GoogleAppsScript.HTML.HtmlOutput {
  const printPage: GoogleAppsScript.HTML.HtmlTemplate = HtmlService.createTemplateFromFile("printPage/index.html")
  const { studentId, studentName, kana } = e.parameter;
  printPage.studentId = studentId;
  printPage.studentName = studentName;
  printPage.kana = kana;
  printPage.timestamp = new Date().toLocaleString();
  return printPage.evaluate();
}

フロント側では印刷用ページを開き、任意の秒数経過後に自動で開いたウィンドウを閉じるといった動きをします。

const printPage = window.open(`https://script.google.com/macros/s/${import.meta.env.VITE_PRINT_SERVICE_DEPLOY_ID}/exec?studentId=${studentId}&studentName=${studentName}&kana=${kana}`, "印刷ページ", "popup")

setTimeout(() => {
    if (!printPage?.closed) {
        printPage?.close()
    }
    success()
}, 3000)

レシートの形式については、@cognitom(Tsutomu Kawamura)さんの記事を参考にして80mmのレシート用紙に必要事項が収まるよう@pageで印刷設定を行いました。

バーコードシンボルの描画については、JsBarcodeを使用しました。Code39で学籍番号を出力しています。

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

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>受付登録票 印刷ページ</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: 'PT Sans', sans-serif;
        }

        p {
            margin: 0;
            padding: 0;
        }

        @page {
            size: 3.2in 15in;
            margin-top: 10px;
            margin-left: 0cm;
            margin-right: 0cm;
        }

        section {
            margin-top: 12px;
        }

        section div.title {
            font-size: 14px;
        }

        section div.content {
            font-size: 24px;
            margin-left: 10px;
        }

        header {
            width: 100%;
            text-align: center;
            -webkit-align-content: center;
            align-content: center;
            vertical-align: middle;
        }

        .center-align {
            text-align: center;
        }

        .barcode-space {
            text-align: center;
            margin: 25px 0px;
        }

        .seal-space {
            height: 900px;
            font-size: 14px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            text-align: center;
        }

        .border {
            border: 0;
            border-bottom: 2px solid #000;
        }
    </style>
</head>

<body>
    <header>
        受付登録票
    </header>

    <section>
        <div class="title">
            受付時刻
        </div>
        <div class="content">
            <?=timestamp ?>
        </div>
    </section>

    <section>
        <div class="title">
            氏名
        </div>
        <div class="content">
            <?=studentName ?></div>
    </section>

    <section>
        <div class="title">
            フリガナ
        </div>
        <div class="content">
            <?=kana ?></div>
    </section>

    <section>
        <div class="title">
            学籍番号
        </div>
        <div class="content" id="studentId">
            <?=studentId ?>
        </div>
    </section>

    <div class="barcode-space">
        <img id="barcode" />
    </div>

    <hr class="border">

    <div class="seal-space">
        <p>製品登録シールを貼り付けてください</p>
        <p>(ガイダンス中に案内があります)</p>
    </div>

    <hr class="border">

    <script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.6/dist/JsBarcode.all.min.js"></script>
    <script>
        window.onload = function () {
            const studentId = document.getElementById("studentId").textContent.trim();
            JsBarcode("#barcode", studentId, {
                format: "CODE39",
                lineColor: "#000",
                width: 1,
                height: 50,
                displayValue: false,
            });

            setTimeout(function () {
                window.print();
            }, 1000);
        }
    </script>

</body>
</html>

また、本番では印刷ダイアログの表示が不要なので、@okoppe8さんの記事を参考にChromeをキオスクモードで起動するbatファイルを作り、これを使いシステムを起動してもらうようにしました。

"C:\Program Files\Google\Chrome\Application\chrome.exe" --kiosk --kiosk-printing --disable-pinch <スプレッドシートのURL>

実際の動作GIFがこちらです。

この際に印刷されたレシートが以下になります。

最終レビュー

機能毎に開発してその都度、担当の方からレビューしてもらい、フィードバックを重ね、ついにお偉いさん方を迎えた場でこのシステムをお見せすることになりました。若干緊張しました。
要件をすべて満たした状態であることを確認してもらい、無事納品できました。
詳細な操作・導入方法は別日に説明を行いました。
保守マニュアル・導入ガイドに関しては、Markdownで作成したものをGitHub上にアップし、誰でも閲覧が可能な状態にしてあります。
取り合えず納品という形までこぎつけましたが、引き継ぎなどやらなければならないことが多いです。

おわりに

今回、人生初の準スクラッチ案件を一人で担当させていただくことになり、必要/不要な機能の選別や大小問わない様々な壁に当たることが多々ありました。しかし、そういった経験で普段の開発では得ることのできない能力というものを身につけることができたと考えています。
例えば、現場の声を直接聞いて機能に落とし込んだり、代替案の提案、運用ノウハウの共有、仕様書・ドキュメント作成など個人開発の範疇を超えた多くの経験をすることができました。
普段の個人開発ではテストやレビューを疎かにして運用でカバーしたりすることが多かったので、今回の経験を今後の個人開発やチーム開発にフィードバックしていきたいです。

Discussion