初めまして。SmartHR でプロダクトエンジニアをしている岩元(@yoiwamoto)です。
本日、自分も開発に携わった SmartHR の「勤怠管理」機能がリリースされました!
近年、「働き方」の多様化はますます進んでおり、勤怠管理システムへのニーズも多様化しています。
勤怠管理機能ではこのようなニーズに応えるために様々な機能を開発していますが、その中の 1 つに打刻時の位置情報取得機能があります。
打刻時の位置情報取得機能について
勤怠管理機能では、ブラウザやスマホアプリ、Slack アプリなどから出勤・退勤・休憩の打刻ができます。
モバイル端末から打刻ができることは従業員にとって便利ですが、オフィス出社が必要な従業員がオフィス外から打刻できてしまうことは避けたいケースもあります。
この課題に対応するために開発したのが、打刻時の位置情報取得機能です。
これは、管理者が打刻時の位置情報を取得するように設定している場合、従業員が打刻をした時にその端末の位置情報を取得して打刻情報に紐付け、後から管理者が閲覧できるようにするものです。
なお、Slack アプリでの打刻の場合は、技術的な制約により位置情報が取得できません。
位置情報を取得するだけであれば簡単なように見えますが、位置情報を取得する Geolocation API には意外とクセがあり、開発時にいくつかハマったポイントがあったので紹介していきます。
Geolocation API の基本
ハマりどころの紹介の前に、Geolocation API の基本を簡単に説明します。
ブラウザ上で端末の位置情報を取得するには、Geolocation API を利用します。
navigator.geolocation.getCurrentPosition((position) => { console.log(position.coords); // { latitude: 35.681236, longitude: 139.767125, accuracy: 10, ... } });
詳しい説明は省略しますが各プロパティは名前の通り、緯度(latitude)、経度(longitude)、精度(accuracy)です。
打刻時にこの API で位置情報を取得し、サーバーに送信するというのが基本設計になります。
取得にめちゃくちゃ時間がかかることがある
getCurrentPosition() は基本的にはほぼ即時に位置情報を返しますが、あくまで非同期 API であり、時間がかかることもあります。
今回、時間がかかる場合の詳細な原因調査はできていませんが、完了までに 10 秒以上かかったケースが何度もあり、時間がかかる可能性を考慮した実装が必要と判断しました。
具体的には、「ユーザーの重要なアクションを位置情報取得処理でブロックしない」ことが重要だと考えています。
例えば今回の打刻に関して言うと、ユーザーが[出勤]ボタンを押した後に JavaScript で位置情報を取得 → 完了を待ってサーバーに打刻種別と位置情報を送信、のような流れで実装してしまうと、仮に位置情報取得に時間がかかった場合、ユーザーはそれを待たなければなりませんし、待たずにブラウザ等を閉じた場合は打刻した事実が失われてしまいます。
これはユーザー体験として非常に問題があります。
これを回避するアプローチは 2 つあります。
位置情報の取得完了を待たずに操作を完了させる
1 つ目は、ユーザーがしようとした操作を優先して完了し、その後に非同期で位置情報を取得するアプローチです。
例えば道案内アプリのようなものであれば、位置情報が取得できないことには案内のしようがないので、取得完了を待つのは自然な流れです。
一方で、打刻に関してユーザーがしたいのは出勤/退勤/休憩の事実の記録であり、位置情報の取得はあくまで付加的な情報です。
ほとんどの場合で位置情報が正しく取得できるべきですが、制御不可能な要因で稀に時間がかかった場合に確実に保持しておくべきなのは、どちらかと言えば打刻の方です。
このような理由で、勤怠管理機能ではまず打刻操作を完了させ、並行して位置情報を取得・送信するように実装しています。これにより、もし位置情報の取得に長い時間がかかってしまった場合でも、ユーザーはとりあえず打刻を完了することができます。
navigator.geolocation.watchPosition() で予め位置情報を取得しておく
しかし、上記のような実装では、裏側で位置情報が取得中であることにユーザーが気付けず、すぐに打刻画面を閉じてしまう可能性があります。
これでは、位置情報の収集率が低下してしまいます。
そこで、事前に位置情報を取得・保持しておいて、打刻が行われたタイミングで即座にこれを送信するという 2 つ目のアプローチも考えられます。
この時、単にページの読み込み時点などで getCurrentPosition() で位置情報を取得・保持してしまうと、例えばページを読み込んだ時点では駅にいて、オフィスに着いた時点で打刻した場合に、駅の位置情報が送信されてしまうという問題が生じます。
端末を持っているユーザーが画面を開きながら移動することを考慮すると、navigator.geolocation.watchPosition() という位置情報の監視 API を利用するのが適切です。
この API は、位置情報の取得を定期的に行うことができるため、ユーザーが画面を開いたまま端末を持っている間は常に最新の位置情報を参照でき、上記の問題が解消されます。
watchPosition() する場合の注意点
今回は、上記の 2 つのアプローチを組み合わせる形でユーザー体験を改善しましたが、気を付けるポイントがあります。
watchPosition() は getCurrentPosition() と同様に、位置情報の権限がまだ得られていない状態で実行すると、ユーザーに権限を求めるダイアログが表示されます(ブラウザの実装によるもの)。
このダイアログが表示される直前には、位置情報を取得することや、それをどのように利用するかをユーザーに伝えておく必要がありますが、watchPosition() のみで位置情報を取得しようとする場合は、打刻画面を開いた時点でいきなり権限を要求するため、ユーザーとしては戸惑うことになります。
そのため、今回は、権限が得られていない状態(初回)ではこのアプローチを諦め、ユーザーが初めて打刻を行った時点で用途の説明・権限要求を行い、getCurrentPosition() で位置情報を取得するようにしました。
代わりに 2 回目以降は watchPosition() で事前取得を行うことで、位置情報の収集率を高める設計としています。

そのままでは <iframe> で利用できない
次に、<iframe> で利用する場合の注意点です。
位置情報はセンシティブなデータであるため、ドキュメントのレスポンスの Permissions-Policy ヘッダなどで利用が制限されます。
これは以下のように機能ごとにアクセス可能なオリジンを指定できるもので、geolocation の既定値は self です。
Permissions-Policy: geolocation=(self "https://example.com")
オリジンコンテキストが異なる場合、Permissions-Policy で Geolocation API のアクセスを明示的に許可しないと利用できません。
<iframe> では、この Permissions-Policy で許可する他に、allow 属性でも同じように許可ができます。
<iframe src="https://example.com" allow="geolocation"></iframe>
SmartHR の勤怠管理機能では、従業員ポータルと呼ばれる SmartHR 本体のホーム画面のような画面にも <iframe> で打刻 UI を埋め込んでいるため、 allow="geolocation" で位置情報アクセスを許可しています。

スマホアプリの WebView 環境でも、そのままでは利用できない
WebView 環境で Geolocation API を利用する場合、アプリケーションを介して権限を得る必要があるため、アプリ側での権限設定が必要になります。
SmartHR では iOS / Android 向けにスマホアプリを提供しており、勤怠管理機能の打刻 UI は、このアプリ上からも WebView で利用できます。
そのため、今回はスマホアプリチームに協力いただき必要な権限設定を行いました。
iOS
iOS では、Info.plist に以下のように権限を追記します。
<key>NSLocationWhenInUseUsageDescription</key> <string>位置情報は、勤怠管理などのSmartHRのサービスで使用されます。</string>
Android
Android では、AndroidManifest.xml に以下のように権限を追記します。
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
権限にレイヤがあるので、JavaScript API だけでは状態が定まらないことがある
位置情報を利用する権限が得られているかどうかは Permissions: query() メソッドで確認できます。
const permission = await navigator.permissions.query({ name: 'geolocation' }); console.log(permission.state); // 'granted' | 'denied' | 'prompt'
詳しい説明は省略しますが、state は権限が得られている状態、拒否されている状態、未使用の状態の 3 つのいずれかになります。
この state を利用して、先述した初回取得時の用途の説明や、watchPosition() を利用するかの分岐などを行います。
しかし、3 で述べたようにスマホアプリ上の WebView で動作している場合、例えば勤怠管理機能で初めて位置情報を要求する前に予めアプリ設定から位置情報権限を拒否されていると、query() では常に 'prompt' が返るという挙動があります。そのため、単に 'prompt' の時にダイアログを表示する、のような実装だと、ユーザーは打刻のたびにダイアログを見ることになります。
また、デスクトップであっても、OS のレイヤで先に位置情報権限を拒否されている場合、state が 'prompt' になります。
現実的にこれを回避するためには、ダイアログを表示したかどうかなどの状態を LocalStorage やバックエンドなどで別途管理しておき、正しい挙動になるように制御する必要があります。
今回はこの問題について、LocalStorage を利用して簡易的に解決しています。
位置情報の精度は端末の状態によって大きく変動する
位置情報の精度は、端末の状態や設定によって大きく変動することがあります。特に以下のような状況では、精度が著しく低下する可能性があります。
- Wi-Fi に接続していない場合(アクセスポイント情報が利用できず、IP アドレスベースでの位置情報取得に頼らざるを得ない)
- スマホアプリを介した位置情報権限の設定で「あいまいな位置情報」を選択した場合
このような場合、10km などの大きな誤差が生じることもあります。特に、IP アドレスベースでの位置情報取得時に顕著です。これは、IP アドレスベースでは市区町村レベルでしか位置を特定できないことに起因します。
幸いなことに、この誤差は Geolocation API の accuracy プロパティで確認可能です。
そのため、例えば精度があまりに低い位置情報では機能が成立しないような場合は、この値が閾値を超える場合に拒否するなど、アプリケーションの要求に応じた分岐が可能です。
おわりに
Geolocation API を使って位置情報を取得・保存する機能は、ブラウザ実装の API で簡単に取得して保存するだけと思いきや意外にもクセが強いことが分かりました。
このエントリがどなたかの参考になれば幸いです。
また、様々な動作環境への対応がありやや複雑な開発になりましたが、ユーザーにとって便利な機能となっていますので、ぜひ SmartHR の勤怠管理機能をご利用いただけると嬉しいです!
We Are Hiring!
SmartHR では一緒に SmartHR を作りあげていく仲間を募集中です!
少しでも興味を持っていただけたら、カジュアル面談でざっくばらんにお話ししましょう!
