LINE Thingsで作った勤怠管理システムを解説してみる #linethings


こんばんわ〜! アドベントカレンダーラストは話題のLINE Thingsネタです。

LINE Things（Developer Trial）を実際にこんな感じで使ってみてるよ〜って話です。

LINE Thingsって？って人はこの辺の記事から漁ってみましょう。

> 参考: [たぶん最速！LINE Things ハンズオン in Node学園祭2018 #nodefest](https://qiita.com/n0bisuke/items/ca6432154a590d0d4daf)

解説してみるって言っておいて初っ端から別の記事に逃がす雑さがウリです（）

そして投稿がギリギリ（12/25 23:40）になってしまったのでアドカレ主の@shinbunbun_くんはヒヤヒヤしたでしょうね！（ごめんね！）

## こんなの作りました

LINE ThingsのLIFFで出勤ボタンを押すと社内チャット（Discord）に通知が行きます。

<img src="https://i.gyazo.com/bba9c827b46dbbca00ed77df79cb1f0d.png" width="200">


オフィスの入り口に起動用のQRも設置してます。デバイスも入り口に置いてあります。余力があれば出勤するとなんちゃらってのをデバイス側実装したいですね。

<img src="https://i.gyazo.com/386e122e5355817bbd198b6e210234d6.jpg" width="300">

Discord側はこんな感じ。

<img src="https://i.gyazo.com/b707ec79e1b7aecd68474d54936c6563.png" width="300">

## ポイント

### 1. リアルにその場所にいないとアプリが起動できない

勤怠管理というか、出社したかどうかの判定として今回利用します。

ポイントは物理的にその場所にいるかどうかの判定がLINE Things（Bluetooth）を使うことでやれるので

```
各々のスマホ <-----ペアリング----> LINE Thingsのデバイス
```

こんな状態になれば打刻が出来て出社判定が出来るといった内容にLINE Thingsの機能は使いやすいです。

### 2. 別のチャットプラットフォーム(Discord)に投げる

社内チャットや業務チャットをSlackやChatworkにしている（うちはDiscrod）組織も多いと思います。

LINEの技術だからLINEで完結させたほうがよくない？って話もよくありますが、こうやって既存の文化に馴染ませる実装をしないと文化を変えることで時間がかかってしまいます。

ケースバイケースでこういう場合はLINEに完結しないほうが良いと思います。

### 3. 実はLINE Thingsの機能は全然使ってない

LINE ThingsやBluetoothって少し難しそうなイメージある気がするのですが、今回はリアルにその場所にいるかどうかの判定にしか使ってません。

なのでデータをデバイスに書き込んだりといったものがないので割とシンプルに作れます。

こういったライトな使い方でのLINE Thingsもけっこうありなのかなと個人的には思っています。（ので試してみてね★）

## LIFF側の実装はVue.jsで

そこまで大した実装ではないのでコード晒していきます。

あと、最近書いた記事でこういったサクッとサンプルをVue.js移植するようにしてるんですけど、かなり見通しが良くなるのでオススメです。

* [WebUSBを使ってブラウザのJavaScriptからArduinoを制御してみよう！](https://qiita.com/n0bisuke/items/b5a90ead155097dce484)
* [Nuxt.jsとSkyWayで1時間でビデオチャットを作ってみる](https://qiita.com/n0bisuke/items/efbe6e09e30ebd941105)


```app.js
'use strict';

var app = new Vue({
    el: '#app',
    data: {
        USER_SERVICE_UUID: '作成したLINE TthingsのサービスUUID',
        LED_CHARACTERISTIC_UUID: '独自定義したUUID', // LINE THings Starterのもので大丈夫
        DISCORD_WEBHOOK_URL: `DiscordのWebhookのURL`,
        bleConnect: false,
        bleStatus: `デバイスに接続されるのをお待ち下さい。`,
        member: 'ちゃんとく', //初期値
        member_action: '出勤', //初期値
        user: {
            image: '',
            userId: ''
        }
    },
    methods: {
        DiscordPost: async function(){
            // Discord
            const message = `${this.member}さんが${this.member_action}しました。（from LINE）`;

            try {
                const res = await axios.post(this.DISCORD_WEBHOOK_URL, {content: message});
                alert('Discordに投稿しました。');                    
            } catch (error) {
                alert('Discordへの投稿に失敗しました。');                
            }
        },
        //BLEが接続できる状態になるまでリトライ
        liffCheckAvailablityAndDo: async function (callbackIfAvailable) {
            try {
                const isAvailable = await liff.bluetooth.getAvailability();
                if (isAvailable) {
                    callbackIfAvailable();
                } else {
                    // リトライ
                    this.bleStatus = `Bluetoothをオンにしてください。`;
                    setTimeout(() => this.liffCheckAvailablityAndDo(callbackIfAvailable), 10000);
                }
            } catch (error) {
                alert('Bluetoothをオンにしてください。');
            }
        },

        //サービスとキャラクタリスティックにアクセス
        liffRequestDevice: async function () {
            const device = await liff.bluetooth.requestDevice();
            await device.gatt.connect();
            const service = await device.gatt.getPrimaryService(this.USER_SERVICE_UUID);
            window.ledCharacteristic = await service.getCharacteristic(this.LED_CHARACTERISTIC_UUID);
            // alert('connect');
            this.bleConnect = true;
            this.bleStatus = `デバイスに接続しました。`;
        },

        initializeLiff: async function(){
            await liff.initPlugins(['bluetooth']);
            this.liffCheckAvailablityAndDo(() => this.liffRequestDevice());
        
            //プロフゲット
            const profile = await liff.getProfile();
            this.user.image = profile.pictureUrl;
            this.user.userId = profile.userId;
        }
    },
    mounted: function(){
        liff.init(
            () => this.initializeLiff(),
            error => location.href = 'https://dotstud.io'
        );
    }
});
```

処理の流れは以下の流れです。

* liff.init: mounted内でliff.init()してあげて、initializeLiff()の呼び出しに続きます。
* initializeLiff: methodsに定義。LIFFのイニシャライズを行います。
* liffCheckAvailablityAndDo: methodsに定義。Bluetoothに接続出来るまでリトライします。
* liffRequestDevice: methodsに定義。サービスとキャラクタリスティックにアクセスして、出来たら画面を表示
* DiscordPost: methodsに定義。Discordにポストします。

最後の方でliff.init()が失敗した場合に`error => location.href = 'https://dotstud.io'`で別サイトに飛ばす実装にしてますが、LIFF以外から開かれようとしたときにはこういった感じでリダイレクトさせるとかはありだと思います。

```index.html
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <link rel="stylesheet" type="text/css" href="liff.css" />
  </head>
  <body>
    <div id="app">
        <h2>勤怠うこさん</h2>
        <img :src="user.image" width="50" />
        <p class="userid">ID: {{this.user.userId}}</p>
        <hr />
        
        <p>
            <span id="status" v-bind:class='{inactive:!bleConnect, success:bleConnect}' v-cloak>
                {{bleStatus}}
            </span>
        </p>
        <hr />

        <div id="controls" v-if="bleConnect === true">
            <input type="radio" id="n0bisuke" value="のびすけ" v-model="member">
            <label for="n0bisuke">のびすけ</label>
            <br>

            <input type="radio" id="chantoku" value="ちゃんとく" v-model="member">
            <label for="chantoku">ちゃんとく</label>
            <br>
            
            <input type="radio" id="uko" value="うこ" v-model="member">
            <label for="uko">うこさん</label>
            <br>
            
            <input type="radio" id="kiki" value="きき" v-model="member">
            <label for="kiki">ききさん</label>
            <br>
            
            <hr>

            <input type="radio" id="office_in" value="出勤" v-model="member_action">
            <label for="office_in">出勤</label>            
            <input type="radio" id="office_out" value="退勤" v-model="member_action">
            <label for="office_out">退勤</label>
            <br>

            <hr>

            <span>{{member}}さんが{{ member_action }}します。</span>

            <hr>
            <button type="button" @click="DiscordPost()">確定</button>
        </div>

        <p>調子が悪い時はデバイスを再起動してね☆</p>
    </div>

    <script src="https://jp.vuejs.org/js/vue.min.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://d.line-scdn.net/liff/1.0/sdk.js"></script>
    <script src="liff.js?v=2"></script>
  </body>
</html>
```

ちなみにUserIdを取ってるのでわざわざユーザーを選ばせなくてよさそうなんですけど、単純に実装とID収集がまにあってないだけです。

## デバイス（Nefry BT）側の実装

今回は（も）Nefry BTで作ってます。特に特殊なことはしてないので、こちらの記事の実装でサービスUUIDを入れて書き込めばOKです。

> 参考: [LINE Things StarterをNefry BTで動かしてみよう #linethings](https://qiita.com/n0bisuke/items/aaf204528a07c0ef248b)

## LINE BeaconじゃなくてLINE Thingsな理由

実際LINE Beaconでも同様のことはやれますが
結論から言うと、無料で出来るのでコスパがいいのと、ほぼノーメンテでいけるのでメンテが楽です。

### LINE Beacon

LINE BeaconはMessaging APIを使ってるのでサーバーサイド実装が必要になります。

サーバーサイド実装が必要になるということはサーバーを用意する必要があってメンテなどをある程度考える必要があります。

nowやherokuを使ったとしても、無料で長期的に運用するときは少し工夫が必要です。

### LINE Things

一方でLINE Thingsはフロントエンドの技術です。

フロントエンドのみで完結するためサーバーもGitHub Pagesをはじめとした無料ホスティングが山ほどあるので、そういったものを使えばいいし、長期運用に際して工夫なども（基本的に）いりません。

また、サーバーサイドのトラブルは無いので、一度動かしてしまえばそこまでメンテの必要もありません。

> LINEのアプリの更新などにともなってLIFFの仕様が変わって...みたいな何かはあるかもしれないので、一概には言えないかもしれないですが

## ホスティングにはSurge.shが最近オススメ

僕がこれ良いよ！って言ってるのは時期によって違う気もしますが、最近だとこいつがおすすめです。

[無料で手軽！コマンド一つで静的サイトホスティングできるSurgeを試してみた。](https://dotstud.io/blog/static-site-hosting-surge/)

無料で以下の内容が使えます。

* カスタムドメインも利用可能
* URLの固定可能
* チーム開発可能
* SSL対応
* コマンド一発でデプロイ

nowみたいに使いやすくてかなりお得感あります。

## そういえばCLIからじゃなくても登録できるようになってました

気づかなかった... これ便利ですね、 ~~というか最初からこうあるべきくらいに思ってしまった~~

<img src="https://i.gyazo.com/740db4ab1f4c8a1e1f7fa7ad48edd5b7.png" width="500">

中の方ありがとう...

## まとめ

皆さんのLIFFアプリやLINE関連アプリケーション作成の参考になれば幸いです！

今回作ってみて思ったのがこれくらいの実装だとLINE Beacon使うよりもだいぶお得感あるのでもっと色々と作ってみたいですね。

これ作るハンズオンとかサクッとやれそうなのでニーズあれば教えてください！（ないかな？）

こんな時間に退勤してますがアドベントカレンダー迎撃で毎年クリスマスはこんな感じですよね！

> <img src="https://i.gyazo.com/b707ec79e1b7aecd68474d54936c6563.png" width="300">

ではでは皆さん今年は[LINE BOOT Awards](https://line-community.me/awards/)などでおせわになりました！
また来年もよろしくお願いいたします :)

良いお年を！