FIELDPIN — 要件定義書 兼 Claude Code 実装指示書
版数 1.0(DBレス構成 / 記事埋め込み型デモ)/ 作成日 2026-08-17 / 発行 ソウゾウ合同会社 用途 SEO記事「【デモで試せる】Claude Codeで位置情報アプリ(GPS)を開発する方法と費用|動画つき資料【2026年】」に組み込む実演デモ
このドキュメントの使い方(Claude Code へ)
これ1枚が、規約・仕様・タスクのすべて。リポジトリ直下に CLAUDE.md として置き、毎回読み込むこと。
- 実装は「13. 実装タスク」の順に進める。フェーズを飛ばさない。
- 各タスクの括弧内は要件ID。着手前に該当章を読むこと。
- 仕様がここに書かれていない場合は、推測で実装せず質問する。
- スコープ外(4章 Won't have)は、思いついても実装しない。
- 「デモだから」を理由に品質を落とす判断はしない。
姉妹プロジェクトとの関係 HIREBASE(求人)/ RELATE(顧客管理)/ CASTA(動画配信)に続く4本目。並べて「同じテンプレ」と思われた時点で失敗。 HIREBASE=明朝・白・余白(読ませる)/ RELATE=ゴシック・白・高密度(操作させる)/ CASTA=ダーク・映像優先(見せる)/ FIELDPIN=地図が全画面・UIは上に浮く・屋外向けの大型UI(現場で使わせる)。 9章は意図的に第4の方向に振っている。トークンを流用しないこと。
目次
- プロジェクト概要
- 記事への組み込み設計
- アーキテクチャ方針
- スコープ定義
- ロールとデモ切替
- 画面一覧とユーザーフロー
- 機能要件
- データ設計
- デザイン要件
- 技術要件・ディレクトリ構成
- 非機能要件・プライバシー
- 費用設計
- 実装タスク
- 受入基準
1. プロジェクト概要
背景と目的
「位置情報アプリを作りたい」という相談の実体は、ほぼ次のいずれかに収まる。
- 訪問介護・配送・警備・設備保守など、現場に出るスタッフの訪問記録を正確に残したい
- 紙とLINEで回している現場報告をアプリにしたい
- GPS打刻で、直行直帰の勤怠を管理したい
一方で、この領域は「作ってみたら使えなかった」が最も多い分野でもある。原因は3つ。
- 位置情報の権限を取れない/拒否されたときの設計がない
- 屋内・地下・山間部でGPSが飛ぶことを前提にしていない
- Webではバックグラウンド追跡ができないことを知らずに設計している
記事とデモの役割は、この3つに正面から答えること。「作れます」ではなく「Webでどこまでできて、どこからネイティブが要るか」を示せることが、この記事の専門性になる。
| 目的 | 内容 |
|---|---|
| 実装力の証明 | 権限フロー・精度処理・ジオフェンス・オフライン記録まで実装した本物を見せる |
| 判断材料の提供 | Webで足りるケースと、ネイティブが必要なケースの線引きを明示する |
| 費用説明の裏付け | 地図タイルの課金構造を、計算式と実測で説明する |
| リード獲得 | デモ内から記事・問い合わせへの導線を常設し、GA4で計測する |
プロダクト定義
| 項目 | 内容 |
|---|---|
| プロダクト名 | FIELDPIN(フィールドピン)※仮称 |
| 一言定義 | 訪問先を地図で管理し、GPSで訪問を記録するフィールドワーク支援アプリ |
| 想定業態 | 訪問介護・訪問看護、配送、設備保守、警備、ルート営業、建設現場巡回 |
| 提供形態 | レスポンシブWebアプリ(PWA)。フロントエンド完結(サーバー側の永続化なし) |
| 主戦場 | 屋外・片手操作・手袋・直射日光。スマホ縦持ちが基本。管理画面のみPC |
| 想定利用者 | 記事の読者。登録なしで、実際に自分の現在地で試せる |
プロダクトコンセプト
「現場の手を止めない。地図の上で終わらせる。」
現場のスタッフはアプリを見るために来ているのではない。訪問先に着き、記録し、次へ向かう——この3動作が地図から離れずに完結すること。画面遷移を減らし、タップを大きくし、片手の親指の届く範囲にすべてを置く。
デモとしての成功条件
- 記事の読者が、自分の現在地で試せる — スマホなら実GPS、PCならシミュレーションで同じ体験ができる
- 権限を拒否しても壊れない — 拒否した状態でも、シミュレーションで全機能を体験できる
- ジオフェンスが体感できる — 訪問先に近づくと自動でチェックイン候補が出る
- 管理側も見られる — スタッフの現在地・訪問実績・軌跡を地図上で確認できる
- 記事に戻れる/問い合わせできる — デモが行き止まりにならない
2. 記事への組み込み設計
この章が本プロジェクト固有の最重要要件。 そして、動画(CASTA)よりも制約が厳しい。
決定的な技術制約:iframe内でGPSは取れない
クロスオリジンの iframe では、Geolocation API は既定で拒否される。 親ページが allow="geolocation" を明示し、かつ親自身が権限を持っている必要がある。さらにブラウザによっては、iframe 内からの権限プロンプト自体が抑制される。
したがって、記事内の埋め込みで実GPSを取ろうとしてはいけない。 取れないか、取れても読者ごとに挙動が変わり、「動かないデモ」として記憶される。
配置方針(2段構え)
| 段 | 配置 | 中身 | GPS |
|---|---|---|---|
| ① 記事内インライン | 記事の冒頭〜中盤、<iframe> |
シミュレーション専用モード。 地図上を仮想スタッフが自動で移動し、ジオフェンス通過でチェックインが発生する様子を見せる | 使わない |
| ② 全画面デモ | 「実際に自分の現在地で試す」カード → 別タブ | アプリ全体。ここで初めて権限を要求する | 使う(拒否時はシミュレーションへ) |
記事内は「勝手に動いて見せるショーケース」、別タブは「自分で試す本番」。役割をはっきり分ける。
埋め込みモード(?embed=1)
同一アプリを、URLパラメータで動作を変える。別リポジトリに分けない。
| 通常モード | 埋め込みモード ?embed=1 |
|
|---|---|---|
| ヘッダー・ナビ・ロール切替バー | 表示 | 非表示 |
| Geolocation API | 使用 | 一切呼ばない(権限プロンプトを出さない) |
| 位置の供給元 | 実GPS or シミュレーション | シミュレーション固定・自動再生 |
| 表示内容 | アプリ全体 | 地図 + 訪問先ピン + 走行中の自分ピン + チェックインの自動発生 |
| 遷移 | 通常 | アプリ内遷移を禁止。 「実際に自分の現在地で試す」は target="_blank" |
| 高さ | 100dvh | 親に合わせる(16:10 程度) |
<iframe
src="https://fieldpin.souzoh-demo.com/?embed=1&sim=tokyo-route-a"
width="100%" style="aspect-ratio:16/10; border:0"
loading="lazy"
title="FIELDPIN 位置情報デモ(シミュレーション)"></iframe>
記事本体のパフォーマンスを壊さない
| ID | 要件 |
|---|---|
| EMB-01 | 記事内 iframe は loading="lazy" とし、ビューポート接近まで読み込まないこと |
| EMB-02 | 埋め込みの初期表示は静止画の地図プレビュー + 「デモを再生」ボタンとし、地図ライブラリとタイルの取得を押下後に開始すること。これは費用要件でもある(12章:地図読み込み1回ごとに課金されるモデルがあるため) |
| EMB-03 | 埋め込みモードのJS初期バンドルは 60KB以下(gzip後)とすること。地図ライブラリは動的import すること |
| EMB-04 | 埋め込み iframe が記事の LCP 要素にならないこと |
| EMB-05 | 埋め込みモードで音を出さないこと。シミュレーションはループ再生とし、タブが非表示になったら停止すること(EMB-02 と併せてタイル消費を抑える) |
「動画つき資料」との一体化
記事タイトルの「動画つき資料」を別途用意せず、デモの体験を撮ったものにする。
- 動画:シミュレーションでの走行 → ジオフェンス通過 → 自動チェックイン → 管理画面での軌跡確認、までを 3分程度で収録
- 資料:本要件定義から抜粋した「Webで足りる/ネイティブが要る の判断表」と「地図タイル費用の計算シート」をPDF化
- デモ内の「資料をダウンロード」からも同じPDFを取得できるようにし、そこを問い合わせ導線に接続する
導線と計測
| ID | 要件 |
|---|---|
| EMB-10 | 全画面デモの上部に、元の記事に戻るリンクを常設すること |
| EMB-11 | 問い合わせ導線を、資料ダウンロード後・管理画面・権限拒否時の案内画面に配置すること。地図操作中の画面には出さない |
| EMB-12 | GA4 で以下を計測すること:demo_open / sim_play / geo_permission_result(granted/denied/unavailable)/checkin_done / geofence_enter / role_switch / doc_download / form_click / line_click |
| EMB-13 | form_click と line_click は既存メディアのキーイベント名と揃えること |
| EMB-14 | geo_permission_result は特に重要。 読者のうち何%が位置情報を許可するかは、記事の続編ネタになると同時に、実案件の設計判断に使える実データになる |
SEO上の扱い
| ID | 要件 |
|---|---|
| EMB-20 | デモは記事とは別サブドメインで配信し、固有の title / description を持つこと |
| EMB-21 | ?embed=1 のURLは noindex とすること |
| EMB-22 | デモの各ページから記事へリンクし、記事側からも nofollow を付けずリンクすること |
| EMB-23 | 記事側に VideoObject(解説動画)と HowTo(開発手順)の構造化データを設置すること |
3. アーキテクチャ方針
基本方針
バックエンドとデータベースを持たない。ただし位置情報の取得だけは実在させる。
| 一般的な構成 | 本プロジェクト |
|---|---|
| PostgreSQL + PostGIS | シードデータ(TypeScript)+ ブラウザ内ストア |
| 認証・スタッフ管理 | デモ用ロール切替(ワンクリック) |
| サーバーへの位置送信 | 送信しない。 ブラウザ内に保持し、リセットで消える |
| プッシュ通知 | アプリ内通知で再現 |
| バックグラウンド追跡 | 実装しない。 できないことを明示する(4章・12章) |
| 現在地の取得 | これだけは本物。 Geolocation API を使う |
| 地図表示 | これも本物。 実際のタイルサービスから地図を描画する |
位置情報だけは実在させる理由
「現在地を取得しました(東京駅)」と表示されるデモには、何の説得力もない。自分の今いる場所が地図に出ることが、このデモの成否そのもの。
ただし実在させるのは取得であって、送信ではない。取得した座標はブラウザから一切出ない。 これは技術方針であると同時に、デモとしての強い訴求点になる(11章)。
位置の供給元を抽象化する(最重要の設計判断)
実GPSとシミュレーションを、同じインターフェースの裏に隠す。
// lib/location/provider.ts
export interface LocationProvider {
start(cb: (fix: Fix) => void, onError: (e: LocationError) => void): void
stop(): void
readonly kind: 'gps' | 'simulation' | 'manual'
}
// 実装は3つ
// GpsProvider … navigator.geolocation.watchPosition
// SimulationProvider … ルートを一定速度で走行。誤差・精度低下も再現
// ManualProvider … 地図をタップした地点を現在地とする(PCでの検証用)
アプリのどこにも navigator.geolocation を直接呼ぶコードを書かない。 すべて LocationProvider 経由。これにより、
- 権限を拒否した読者も、シミュレーションで全機能を体験できる
- PCの読者も同じ体験ができる(記事の読者はPC比率も高い)
- E2Eテストが決定的に書ける(実GPSに依存しない)
- 埋め込みモードでは Provider を差し替えるだけで Geolocation を呼ばずに済む
この抽象化は記事の「方法」パートの中核コンテンツにもなる。
GPSの現実に対処する
| 現象 | 対処 |
|---|---|
| 屋内・地下で精度が落ちる(accuracy 数百m) | 精度円を必ず描画し、閾値超過時は「精度が低い」と明示。チェックインは可能だが記録に精度を残す(FR-207) |
| 座標が飛ぶ(ドリフト) | 直前の位置からの移動速度が非現実的な場合(既定 120km/h 超)は棄却する(FR-105) |
| 初回取得が遅い(コールドスタート) | 低精度で即座に返し、高精度が来たら差し替える2段構え。取得中はスケルトンではなく進捗を言葉で示す(FR-103) |
| 権限が拒否・ブロックされている | 拒否状態を検知し、ブラウザ別の解除手順を提示。同時にシミュレーションへの導線を出す(FR-106) |
| 電波が切れる | オフラインでも記録でき、キューに溜めて復帰時に同期する(FR-403) |
永続化の範囲
| 対象 | 挙動 |
|---|---|
| シードデータ(訪問先・スタッフ・予定) | 初期投入。管理画面から編集可能 |
| チェックイン記録・訪問報告・軌跡 | localStorage に保存。リロードしても残る |
| 権限の状態 | ブラウザが保持。アプリ側は Permissions API で読むだけ |
| 取得した座標 | localStorage にのみ保存。外部へ送信しない |
| リセット | 「デモをリセット」で localStorage をクリア |
擬似ディレイ は 150〜400ms。ただし位置取得・地図操作にはディレイを入れない。
4. スコープ定義
現場スタッフ向け機能
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-F01 | 現在地表示 | 地図上に自分の位置と精度円、進行方向を表示 | Must |
| F-F02 | 権限フロー | 事前説明 → 許可要求 → 結果別の案内(許可/拒否/ブロック/非対応) | Must |
| F-F03 | シミュレーションモード | 実GPSなしで全機能を体験。ルート走行、手動での位置指定 | Must |
| F-F04 | 訪問先マップ | 訪問先をピン表示。状態(未訪問/訪問中/完了/スキップ)で色分け | Must |
| F-F05 | 今日の予定 | 訪問予定の一覧(ボトムシート)。地図と連動 | Must |
| F-F06 | ジオフェンス | 訪問先の設定半径に入ると通知し、チェックインを促す | Must |
| F-F07 | チェックイン/チェックアウト | 位置と時刻を記録。距離が離れている場合は理由入力を求める | Must |
| F-F08 | 訪問報告 | テンプレート項目、自由記述、写真添付、次回申し送り | Must |
| F-F09 | 写真の位置情報付与 | 撮影/選択した写真に、その時点の座標と時刻を紐付ける | Should |
| F-F10 | ルート案内 | 次の訪問先までの経路を外部地図アプリで開く | Should |
| F-F11 | 訪問順の最適化 | 現在地から近い順の並び替え(総当たり最近傍法) | Should |
| F-F12 | 移動軌跡の記録 | 業務中の軌跡を記録し、地図上に線で表示 | Should |
| F-F13 | オフライン対応 | 圏外でも記録でき、復帰時にキューを同期 | Must |
| F-F14 | 活動履歴 | 自分の過去の訪問記録、日付別 | Should |
| F-F15 | 緊急連絡 | 現在地を含む連絡を管理者へ送信(デモ内で完結) | Could |
管理者向け機能
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-M01 | 稼働マップ | スタッフ全員の現在地を地図上に表示。最終更新時刻付き | Must |
| F-M02 | スタッフ一覧 | 稼働状況、進捗(3/8件)、遅延アラート | Must |
| F-M03 | 軌跡の再生 | 指定した日のスタッフの軌跡を、地図上でタイムライン再生 | Must |
| F-M04 | 訪問先マスタ | 訪問先の登録・編集、地図上でのピン配置、ジオフェンス半径設定 | Must |
| F-M05 | 訪問予定の作成 | スタッフへの割当、日付・時間帯、繰り返し設定 | Must |
| F-M06 | 訪問記録の確認 | 報告内容、写真、チェックイン位置と訪問先の距離 | Must |
| F-M07 | 実績ダッシュボード | 訪問完了率、平均滞在時間、移動距離、遅延件数 | Should |
| F-M08 | エリア分析 | 訪問先の密度をヒートマップ表示 | Should |
| F-M09 | 異常検知 | 訪問先から離れた場所でのチェックイン、長時間の停滞、予定超過 | Should |
| F-M10 | CSVエクスポート | 訪問記録の書き出し | Should |
| F-M11 | 位置情報の保持設定 | 取得間隔、保存期間、記録のON/OFF | Must |
共通・基盤
| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-S01 | ロール切替 | 現場スタッフ/管理者/システム管理 をワンクリック切替 | Must |
| F-S02 | 埋め込みモード | ?embed=1 でシミュレーション専用表示(2章) |
Must |
| F-S03 | 資料ダウンロード | 判断表・費用計算シートのPDF | Must |
| F-S04 | 記事への導線 | 元記事へのリンク、問い合わせ導線 | Must |
| F-S05 | デモリセット | localStorage をクリアして初期状態へ | Must |
| F-S06 | PWA | ホーム画面追加、オフライン起動 | Should |
| F-S07 | ガイドツアー | 初回訪問時に「何を試せるか」を3ステップで案内 | Should |
対象外(Won't have)
| 項目 | 理由 |
|---|---|
| データベース・バックエンドAPI | 3章の方針に基づく |
| 本物の認証 | ロール切替で代替 |
| バックグラウンドでの位置追跡 | Webでは実現できない。 できないことを明示するのが記事の価値(12章で解説) |
| 経路探索・ナビゲーションの自前実装 | 外部地図アプリへの受け渡しで代替 |
| 高度な配車最適化(VRP) | 最近傍法による並び替えに留める |
| リアルタイムの相互位置共有(WebSocket) | ロール切替時に反映される形で再現 |
| 屋内測位(ビーコン・Wi-Fi測位) | 対象外 |
| 実際のプッシュ通知 | アプリ内通知で再現 |
| ネイティブアプリ | PWAで対応。ネイティブが必要なケースは12章で明示 |
スコープリスク: 位置情報は「追跡もしたい」「ナビも欲しい」「配車最適化も」と広がりやすい。特にバックグラウンド追跡は要望が出やすいが、Webでは技術的に不可能であり、ネイティブアプリ開発(別見積)になる。この線引きこそが記事の核心なので、曖昧にしない。
5. ロールとデモ切替
ロール定義
| ロールID | 名称 | 見える範囲 | 特徴 |
|---|---|---|---|
staff |
現場スタッフ | 自分の予定・記録のみ | 地図中心のモバイルUI。実際に位置を取得する |
manager |
管理者 | 自チーム全員 | 稼働マップ、軌跡再生、訪問先マスタ、実績 |
admin |
システム管理 | 全社 | 上記 + 位置情報の保持設定、スタッフ管理 |
位置の供給元も切り替えられる(このデモ固有)
ロールとは別に、位置の供給元を切り替えるコントロールを持つ。
| モード | 挙動 | 想定 |
|---|---|---|
gps |
実際の Geolocation API | スマホで読んでいる人 |
simulation |
用意されたルートを自動走行 | 権限拒否/PC/埋め込み |
manual |
地図をタップした地点を現在地にする | PCでジオフェンスを試したい人 |
初回起動時、この3つを選ばせる。「今すぐ試す(位置情報を使う)/デモ走行を見る/地図をタップして試す」。いきなり権限プロンプトを出さない(FR-101)。
ロールとモードを跨ぐ体験(必ず動くようにする)
- シミュレーションで走行 → 訪問先に近づく → ジオフェンス通知 → チェックイン → 管理者に切替 → 稼働マップにその記録が反映されている
- 実GPSで自分の現在地を表示 → 地図タップで任意の訪問先を作成 → ジオフェンス半径を50mに → その場でチェックイン
- オフライン(機内モード)でチェックイン → 未同期バッジが付く → オンラインに戻す → 自動同期される
- 管理者で軌跡再生 → スタッフが今日たどった経路がタイムラインで再生される
- 管理者が訪問先の位置を移動 → スタッフに切替 → ジオフェンスの判定位置が変わっている
デモ切替バー(F-S01)
画面上部に固定表示。埋め込みモードでは非表示。
- ロール切替(3ロール)
- 位置モード切替(GPS/シミュレーション/手動)と、現在の精度表示
- 「デモをリセット」(確認ダイアログ付き)
- 元記事へ戻るリンク
- 「これはデモです。位置情報は送信されません」の明示
デザイン上の扱い: アプリ本体が地図全画面なので、切替バーは地図に重ならない上端固定の濃色帯とし、地図UIと明確に区別する。
6. 画面一覧とユーザーフロー
全26画面。画面IDはディレクトリ構成と 1:1 で対応させる。
現場スタッフ画面(モバイル最適化)
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-001 | 起動・モード選択 | / |
3つの体験モード選択。権限プロンプトは出さない |
| SC-002 | 権限リクエスト | /permission |
事前説明(何に使い、どこへ送らないか)→ 許可ボタン |
| SC-003 | 権限拒否・ブロック | /permission/blocked |
ブラウザ別の解除手順、シミュレーションへの導線 |
| SC-010 | メインマップ | /map |
全画面地図、現在地+精度円、訪問先ピン、ボトムシート、FAB |
| SC-011 | 今日の予定(シート) | /map 内 |
ボトムシート3段階(畳む/中/全開)。訪問先リスト、進捗 |
| SC-012 | 訪問先詳細(シート) | /map/places/[id] |
名称、住所、距離、ジオフェンス半径、前回訪問、チェックインボタン |
| SC-013 | チェックイン確認 | モーダル | 現在地と訪問先の距離、精度、離れている場合の理由入力 |
| SC-014 | 訪問報告 | /visits/[id]/report |
テンプレート項目、自由記述、写真添付、次回申し送り |
| SC-015 | チェックアウト | モーダル | 滞在時間、位置、報告未記入の警告 |
| SC-016 | 訪問順の最適化 | /map/optimize |
現在地から近い順の並び替え提案、適用前後の総距離比較 |
| SC-020 | 活動履歴 | /history |
日付別の訪問記録、地図サムネイル |
| SC-021 | 履歴詳細 | /history/[date] |
その日の軌跡、訪問先、報告内容 |
| SC-030 | 設定 | /settings |
位置モード、取得間隔、通知、軌跡記録のON/OFF |
| SC-031 | 未同期キュー | /settings/queue |
オフライン中の記録一覧、手動同期 |
| SC-090 | 埋め込みモード | /?embed=1 |
シミュレーション自動走行。UIは地図とピンのみ |
管理者画面
| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-100 | 稼働マップ | /admin |
全スタッフの現在地、最終更新時刻、進捗バッジ、遅延アラート |
| SC-101 | スタッフ一覧 | /admin/staff |
稼働状況、進捗、本日の訪問件数、移動距離 |
| SC-102 | スタッフ詳細 | /admin/staff/[id] |
本日の予定と実績、現在地、連絡 |
| SC-110 | 軌跡再生 | /admin/tracks |
日付・スタッフ選択、地図上でのタイムライン再生(速度変更可) |
| SC-120 | 訪問先マスタ | /admin/places |
一覧+地図の2ペイン。ピンのドラッグで位置修正 |
| SC-121 | 訪問先編集 | /admin/places/[id] |
名称、住所、座標、ジオフェンス半径(地図上で円をドラッグ)、担当、メモ |
| SC-130 | 訪問予定 | /admin/schedule |
カレンダー/リスト、スタッフへの割当、繰り返し |
| SC-140 | 訪問記録 | /admin/visits |
一覧、絞り込み、チェックイン位置と訪問先の距離、写真 |
| SC-141 | 訪問記録詳細 | /admin/visits/[id] |
報告内容、写真(位置情報付き)、地図上の実際のチェックイン地点 |
| SC-150 | 実績ダッシュボード | /admin/reports |
完了率、平均滞在時間、移動距離、遅延件数、期間指定 |
| SC-151 | エリア分析 | /admin/reports/area |
訪問先密度のヒートマップ |
| SC-160 | 位置情報の設定 | /admin/settings/location |
取得間隔、保存期間、軌跡記録の可否、取得目的の明示文 |
主要フロー
フローA:記事の読者が位置情報に触れる(最重要)
SEO記事を読んでいる
↓
記事内の埋め込み(静止画の地図 + 「デモを再生」)
↓
再生 ── 仮想スタッフが地図上を走行、訪問先に接近
↓ ★ ジオフェンス円に入ると自動でチェックインが発生する様子が見える
「実際に自分の現在地で試す」→ 別タブ
↓
SC-001 モード選択 ──「今すぐ試す(位置情報を使う)」を選択
↓
SC-002 権限リクエスト ── ★ 何に使い、どこへも送らないことを先に説明
↓
ブラウザの権限プロンプト
├ 許可 → SC-010 メインマップ。自分の現在地が地図に出る
└ 拒否 → SC-003 解除手順 + 「デモ走行で試す」導線 → シミュレーションで同じ体験
↓
SC-010 ── 近くに仮の訪問先が自動生成される(★ どこで開いても体験が成立する)
↓
訪問先に近づく or 地図タップで移動 → ジオフェンス通知 → チェックイン
↓
SC-014 訪問報告 → 写真添付 → 完了
↓
ロール切替「管理者」→ SC-100 稼働マップ
↓ ★ たった今の自分のチェックインが、管理側に反映されている
資料ダウンロード or 元記事に戻る
フローB:ジオフェンスとチェックイン
SC-010 メインマップ ── 訪問先ピンとジオフェンス円(半径100m)が見える
↓
移動して円の内側に入る
↓
★ ジオフェンス進入イベント
├ 画面上部にバナー「〇〇邸に到着しました」+ チェックインボタン
├ バイブレーション(対応端末のみ)
└ 通知センターに記録
↓
チェックインボタン
↓
SC-013 確認モーダル
現在地と訪問先の距離(例:42m)
位置の精度(例:±15m)
★ 距離が半径を超えている場合は「離れた場所での記録」として理由入力を必須化
↓
チェックイン完了 ── 滞在時間の計測開始、訪問先ピンが「訪問中」に変わる
↓
SC-014 訪問報告 ── 報告を書く(下書きは自動保存)
↓
SC-015 チェックアウト ── 滞在時間を確定。報告未記入なら警告
↓
訪問先ピンが「完了」に。次の訪問先へのルート案内ボタンが出る
フローC:オフラインでの記録と同期
SC-010 メインマップ(オンライン)
↓
機内モードにする(または DevTools でオフラインに)
↓
★ 画面上部にオフラインバナー「オフラインです。記録は端末に保存されます」
↓
チェックイン → 訪問報告 → 写真添付
↓ すべて成功する。ただし各記録に「未同期」バッジが付く
SC-031 未同期キュー ── 3件の未同期記録が並ぶ
↓
オンラインに戻す
↓
★ 自動で同期開始。バナーが「同期中… 2/3」→「同期しました」
↓
未同期バッジが消える
↓
ロール切替「管理者」→ SC-140 訪問記録に反映されている
7. 機能要件
7.1 位置情報の取得と権限(このデモの心臓部)
| ID | 要件 | 優先 |
|---|---|---|
| FR-101 | 起動直後に権限プロンプトを出さないこと。 SC-001 で3つの体験モードを提示し、ユーザーが選択してから要求すること | P1 |
| FR-102 | 権限要求の前に、何のために使い、どこへも送信しないことを画面上で説明すること(SC-002) | P1 |
| FR-103 | 位置取得は2段構えとすること:まず enableHighAccuracy: false で素早く概位置を取得し、続いて高精度取得に切り替えて差し替えること。取得中は「位置を特定しています…」と状態を言葉で示すこと |
P1 |
| FR-104 | 取得した位置に精度円を必ず描画すること。 精度(±Nm)を数値でも表示すること | P1 |
| FR-105 | ドリフト対策:直前の位置からの推定移動速度が閾値(既定120km/h)を超える座標は棄却すること。棄却したことをログに残し、設定画面で確認できること | P1 |
| FR-106 | 権限の状態を Permissions API で判定し、denied の場合は ブラウザ別(iOS Safari / Android Chrome / PC Chrome / Firefox)の解除手順を提示すること。同時にシミュレーションへの導線を出すこと(SC-003) |
P1 |
| FR-107 | Geolocation 非対応・HTTPS でない環境を検知し、専用の案内を出すこと。白画面や無言の失敗を起こさないこと | P1 |
| FR-108 | タイムアウト(既定15秒)で取得できない場合、再試行とシミュレーションへの切替を提示すること | P1 |
| FR-109 | 位置の取得間隔を設定できること(高頻度5秒/標準15秒/省電力60秒)。現在の設定がバッテリーに与える影響を1行で説明すること | P2 |
| FR-110 | 端末の向き(deviceorientation)が取得できる場合、現在地マーカーに進行方向の扇形を表示すること。取得できない場合は円のみとすること |
P2 |
| FR-111 | navigator.geolocation を直接呼ぶコードをアプリ内に書かないこと。 すべて LocationProvider を経由すること(3章) |
P1 |
| FR-112 | 画面が非表示(visibilitychange)になったら位置の監視を停止し、復帰時に再開すること。バッテリー消費を抑える |
P1 |
7.2 シミュレーション・手動モード
| ID | 要件 | 優先 |
|---|---|---|
| FR-151 | シミュレーションモードで、あらかじめ用意したルート上を一定速度で走行すること。速度(1x/2x/4x)を変更できること | P1 |
| FR-152 | シミュレーションでも精度の揺らぎと座標の誤差を再現すること。 完璧な直線移動にしないこと | P1 |
| FR-153 | シミュレーション用ルートを複数用意し、URLパラメータ(?sim=tokyo-route-a)で指定できること |
P2 |
| FR-154 | 手動モードで、地図をタップした地点を現在地として扱えること。連続タップで移動を再現できること | P1 |
| FR-155 | モードの切替は、記録済みのデータを失わずに行えること | P1 |
| FR-156 | 現在どのモードで動作しているかを、地図上に常時表示すること。実GPSとシミュレーションを誤認させないこと | P1 |
7.3 地図表示
| ID | 要件 | 優先 |
|---|---|---|
| FR-201 | 地図はベクタータイルで描画し、ピンチズーム・回転・傾きに対応すること | P1 |
| FR-202 | 訪問先をピンで表示し、状態(未訪問/訪問中/完了/スキップ/遅延)で色と形の両方を変えること(色のみに依存しない) | P1 |
| FR-203 | ピンが密集する場合はクラスタリングし、ズームで展開すること | P2 |
| FR-204 | 訪問先のジオフェンス円を地図上に描画すること。現在地が円内にあるときは塗りを濃くすること | P1 |
| FR-205 | 「現在地へ戻る」ボタンを常時配置すること。押下で現在地を中心に、適切なズームで移動すること | P1 |
| FR-206 | 地図の初期表示範囲は、現在地と当日の訪問先すべてが収まるように自動調整すること | P1 |
| FR-207 | 精度が閾値(既定100m)より悪い場合、精度円を警告色にし、「位置の精度が低い状態です」と表示すること | P1 |
| FR-208 | 移動軌跡を線で描画できること。設定でON/OFFできること | P2 |
| FR-209 | 地図のスタイル(標準/航空写真風/高コントラスト)を切り替えられること。高コントラストは直射日光下での視認性のために用意する | P2 |
| FR-210 | 地図の初期化回数を最小化すること。画面遷移で地図インスタンスを破棄・再生成しないこと(費用要件でもある、12章) | P1 |
7.4 ジオフェンスとチェックイン
| ID | 要件 | 優先 |
|---|---|---|
| FR-301 | 各訪問先にジオフェンス半径(既定100m、20〜500mで設定可)を持たせること | P1 |
| FR-302 | 位置更新のたびに全訪問先との距離を計算し、進入・退出イベントを発火させること(Haversine距離を使用) | P1 |
| FR-303 | 判定にヒステリシスを設けること:進入は半径内、退出は半径×1.2 を超えたとき。境界での連続発火を防ぐ | P1 |
| FR-304 | 進入時、画面上部にバナー、通知センターへの記録、バイブレーション(対応端末のみ)で通知すること | P1 |
| FR-305 | チェックイン時に、座標・時刻・精度・訪問先からの距離 を記録すること | P1 |
| FR-306 | チェックイン位置が訪問先から半径を超えて離れている場合、「離れた場所での記録」として理由入力を必須化すること。 ブロックはしない(現場で入れない事情は常にある) | P1 |
| FR-307 | チェックアウト時に滞在時間を確定し、報告が未記入の場合は警告すること(ブロックはしない) | P1 |
| FR-308 | 二重チェックインを防止すること。既にチェックイン中の訪問先がある場合は、先にチェックアウトを促すこと | P1 |
| FR-309 | 訪問をスキップでき、理由(不在/キャンセル/その他)を記録できること | P2 |
| FR-310 | ジオフェンスの判定は画面が表示されている間のみ行うこと。バックグラウンドでは動作しないことを設定画面で明示すること | P1 |
7.5 訪問報告・写真・オフライン
| ID | 要件 | 優先 |
|---|---|---|
| FR-401 | 訪問報告はテンプレート項目(選択式)+ 自由記述 + 次回申し送り で構成すること。テンプレートは管理者が編集できること | P1 |
| FR-402 | 報告の下書きを入力停止から2秒後に自動保存すること。アプリを閉じても失われないこと | P1 |
| FR-403 | オフライン時もチェックイン・報告・写真添付ができること。 記録に「未同期」を付け、キューに保持すること | P1 |
| FR-404 | オンライン復帰を検知し、キューを自動で同期すること。同期の進捗をバナーで表示すること | P1 |
| FR-405 | 未同期キューを一覧で確認でき、手動同期・個別削除ができること(SC-031) | P1 |
| FR-406 | オフライン状態を画面上部のバナーで常時明示すること | P1 |
| FR-407 | 写真を撮影・選択でき、その時点の座標・時刻・精度を紐付けること。写真上に位置情報が付与されたことを表示すること | P2 |
| FR-408 | 写真は端末内で圧縮(長辺1600px・JPEG品質80)してから保持し、容量を抑えること | P2 |
| FR-409 | 写真から Exif の位置情報を読み取らないこと。アプリが記録した座標のみを使うこと(Exif の有無に挙動を依存させない) | P2 |
7.6 ルート・並び替え
| ID | 要件 | 優先 |
|---|---|---|
| FR-501 | 訪問先一覧を「現在地から近い順」「予定時刻順」「訪問先名順」で並び替えられること | P1 |
| FR-502 | 最近傍法で訪問順を最適化する提案を出し、適用前後の総移動距離を比較表示すること | P2 |
| FR-503 | 最適化はあくまで提案とし、適用するかはユーザーが選べること。自動で並びを変えないこと | P2 |
| FR-504 | 次の訪問先までのルート案内を、外部地図アプリ(Google Maps / Apple Maps)で開けること。端末に応じて適切な方を提示すること | P2 |
| FR-505 | 移動軌跡を記録し、総移動距離を算出すること。連続する座標間の距離を合算し、精度の悪い点は除外すること | P2 |
7.7 管理者機能
| ID | 要件 | 優先 |
|---|---|---|
| FR-601 | 稼働マップに全スタッフの現在地を表示し、最終更新からの経過時間を併記すること(「3分前」)。古い情報を現在地として誤認させないこと | P1 |
| FR-602 | スタッフの進捗(完了/予定件数)と遅延をピン上のバッジで示すこと | P1 |
| FR-603 | 軌跡再生:日付とスタッフを選び、地図上でタイムライン再生できること。再生速度を変更でき、任意の時点にシークできること | P1 |
| FR-604 | 訪問先マスタを、一覧と地図の2ペインで編集できること。地図上でピンをドラッグして座標を修正できること | P1 |
| FR-605 | ジオフェンス半径を、地図上で円をドラッグして視覚的に設定できること | P1 |
| FR-606 | 訪問予定をスタッフに割り当てられること。日付・時間帯・繰り返し(毎週/隔週)を設定できること | P1 |
| FR-607 | 訪問記録一覧で、チェックイン位置と訪問先の距離を列表示し、離れているものを絞り込めること | P1 |
| FR-608 | 訪問記録詳細で、実際のチェックイン地点を地図上に表示し、訪問先との位置関係を可視化すること | P1 |
| FR-609 | 実績ダッシュボード:訪問完了率、平均滞在時間、総移動距離、遅延件数を期間指定で表示すること | P2 |
| FR-610 | エリア分析:訪問先の密度をヒートマップで表示すること | P2 |
| FR-611 | 異常検知:訪問先から大きく離れたチェックイン、長時間の停滞、予定超過 を一覧で提示すること | P2 |
| FR-612 | 訪問記録をCSVエクスポートできること | P2 |
| FR-613 | 位置情報の取得間隔・保存期間・軌跡記録の可否を設定でき、「取得目的の明示文」を編集できること。 この設定内容がスタッフの設定画面にも表示されること | P1 |
7.8 デモ基盤
| ID | 要件 | 優先 |
|---|---|---|
| FR-701 | 3ロール(現場スタッフ/管理者/システム管理)をワンクリックで切り替えられること | P1 |
| FR-702 | 位置モード(GPS/シミュレーション/手動)を切り替えられ、現在のモードと精度を常時表示すること | P1 |
| FR-703 | 「デモをリセット」で localStorage をクリアすること | P1 |
| FR-704 | 記録がリロード後も保持されること | P1 |
| FR-705 | 現在地の近くに仮の訪問先を自動生成すること。 どの地域で開いても体験が成立するようにすること | P1 |
| FR-706 | 元記事へ戻るリンクと資料ダウンロードを常設すること | P1 |
| FR-707 | ロール権限外の画面にアクセスした場合、案内画面から切り替えられること。素の404を出さないこと | P1 |
| FR-708 | データスコープは lib/repo/_scope.ts に集約し、全リポジトリメソッドが通すこと |
P1 |
| FR-709 | 初回訪問時に3ステップのガイドツアーを表示し、「今後表示しない」を選べること | P2 |
8. データ設計
型定義(lib/types/)
// ---- 位置の基本型 ----
type Coord = { lat: number; lng: number }
type Fix = { // 位置の1点
coord: Coord
accuracyM: number // 精度(メートル)
altitude?: number
heading?: number // 進行方向(度)
speedMps?: number
source: 'gps' | 'simulation' | 'manual'
at: string // ISO8601
}
type LocationError =
| { kind: 'permission_denied' }
| { kind: 'position_unavailable' }
| { kind: 'timeout' }
| { kind: 'insecure_context' } // HTTPSでない
| { kind: 'unsupported' }
type PermissionState = 'prompt' | 'granted' | 'denied' | 'unsupported'
// ---- 訪問先 ----
type Place = {
id: string
name: string
nameKana: string
address: string
coord: Coord
geofenceRadiusM: number // 既定100、20〜500
categoryId: string
assignedStaffIds: string[]
contactName: string
contactPhone: string
note: string
isTemporary: boolean // ★ 現在地近くに自動生成した仮の訪問先(FR-705)
createdAt: string
updatedAt: string
}
// ---- スタッフ・予定 ----
type Staff = {
id: string
name: string
avatarUrl: string
role: 'staff' | 'manager' | 'admin'
teamId: string
isOnDuty: boolean
lastFix?: Fix // 最終位置。管理者の稼働マップで使用
isActive: boolean
}
type ScheduleItem = {
id: string
placeId: string
staffId: string
date: string // YYYY-MM-DD
plannedStartAt?: string // 予定時刻
plannedDurationMin: number
sortOrder: number
recurrence?: { type: 'weekly' | 'biweekly'; weekdays: number[] }
status: 'planned' | 'in_progress' | 'done' | 'skipped'
}
// ---- 訪問記録 ----
type Visit = {
id: string
scheduleItemId: string
placeId: string
staffId: string
// --- チェックイン ---
checkInAt: string
checkInFix: Fix
checkInDistanceM: number // ★ 訪問先からの距離(FR-305)
isRemoteCheckIn: boolean // 半径外だったか
remoteReason?: string // 半径外の場合の理由(FR-306)
// --- チェックアウト ---
checkOutAt?: string
checkOutFix?: Fix
stayMinutes?: number
// --- 報告 ---
report?: VisitReport
photos: VisitPhoto[]
// --- 同期 ---
syncState: 'synced' | 'pending' // ★ オフライン記録(FR-403)
createdAt: string
}
type VisitReport = {
templateValues: Record<string, string | string[] | boolean>
freeText: string
handover: string // 次回申し送り
savedAt: string
}
type VisitPhoto = {
id: string
objectUrl: string // セッション内のみ有効
fileName: string
sizeBytes: number
takenFix: Fix // ★ 撮影時点の位置(FR-407)
at: string
}
type ReportTemplate = {
id: string
name: string
fields: { id: string; label: string; type: 'select' | 'multiselect' | 'text' | 'checkbox'; options?: string[]; isRequired: boolean }[]
}
// ---- 軌跡 ----
type Track = {
id: string
staffId: string
date: string
fixes: Fix[] // 間引き済み(8章の方針参照)
totalDistanceM: number
startedAt: string
endedAt?: string
}
// ---- ジオフェンスイベント ----
type GeofenceEvent = {
id: string
placeId: string
staffId: string
type: 'enter' | 'exit'
fix: Fix
distanceM: number
at: string
}
// ---- シミュレーション ----
type SimRoute = {
id: string // 'tokyo-route-a' 等
name: string
path: Coord[] // 通過点
speedKmh: number
placeIds: string[] // このルート上に配置する訪問先
}
// ---- 設定 ----
type LocationSettings = {
intervalSec: 5 | 15 | 60
trackRecording: boolean
retentionDays: number // 保存期間
purposeStatement: string // ★ 取得目的の明示文(FR-613)
accuracyThresholdM: number // 既定100
driftSpeedThresholdKmh: number // 既定120
}
// ---- 同期キュー ----
type SyncQueueItem = {
id: string
targetType: 'visit' | 'report' | 'photo'
targetId: string
createdAt: string
attempts: number
lastError?: string
}
軌跡データの間引き(重要)
位置を15秒間隔で8時間記録すると1,920点になる。そのまま保持すると localStorage が破綻する。
| ID | 方針 |
|---|---|
| DATA-01 | 軌跡は Douglas-Peucker 法(許容誤差10m)で間引いてから保存すること |
| DATA-02 | 精度が閾値より悪い点は軌跡に含めないこと |
| DATA-03 | 1日あたりの保持点数に上限(既定500点)を設け、超過分は間引きの許容誤差を上げて再圧縮すること |
| DATA-04 | localStorage の使用量を設定画面で可視化し、上限接近時に警告すること |
シードデータ(lib/seed/)
| ファイル | 内容 | 件数 |
|---|---|---|
staff.ts |
スタッフ(現場5・管理者1・システム管理1)、チーム2 | 7名 |
places.ts |
訪問先(東京都心部に配置。密集エリアと点在エリアを作る) | 60件 |
schedule.ts |
訪問予定(過去14日 + 本日 + 翌週。本日分は各スタッフ6〜9件) | 約500件 |
visits.ts |
訪問記録(過去14日分。半径外チェックインを数件混ぜる) | 約400件 |
tracks.ts |
軌跡(過去14日分。間引き済み) | 70本 |
simRoutes.ts |
シミュレーションルート(都心・郊外・住宅街の3種) | 3本 |
templates.ts |
報告テンプレート(訪問介護向け/設備保守向け) | 2件 |
シード作成のルール(品質を左右する)
- 訪問先は実在住所を使わない。 実在の個人宅・企業を指すデータを作らない。地名は実在でよいが、名称は架空にする(「〇〇邸」は避け「A様邸」等)
- 座標は道路上・建物上に自然に配置する。 海の上や線路の真上にピンが立つと一瞬で嘘だと分かる
- 軌跡は道なりにする。 直線で結ばない。主要道路に沿った通過点を打つ
- 半径外チェックインを数件混ぜる。 異常検知と距離列が意味を持つようにする
- 遅延しているスタッフを1名作る。 全員順調だとアラート機能が動いて見えない
- 日付は現在日時からの相対で生成する。固定日付を埋め込まない
- 本日の予定に「訪問中」の1件を含める(起動直後の画面が空にならないように)
ストアとリポジトリ
lib/
├── types/ seed/
├── location/ # ★ 位置の抽象化(3章)
│ ├── provider.ts # LocationProvider インターフェース
│ ├── gps-provider.ts # navigator.geolocation を呼ぶ唯一の場所
│ ├── simulation-provider.ts
│ ├── manual-provider.ts
│ ├── permission.ts # Permissions API のラッパー
│ └── filter.ts # ドリフト棄却・精度フィルタ
├── geo/ # 純粋な計算。副作用なし・テスト必須
│ ├── distance.ts # Haversine
│ ├── geofence.ts # 進入退出判定(ヒステリシス付き)
│ ├── simplify.ts # Douglas-Peucker
│ ├── bounds.ts # 表示範囲の算出
│ └── nearest.ts # 最近傍法による並び替え
├── store/ { session, data, settings, sync, ui }
└── repo/ # ★ コンポーネントが触るのはここだけ
├── _delay.ts _scope.ts
├── places.ts staff.ts schedule.ts visits.ts tracks.ts reports.ts sync.ts
リポジトリ層の規約(厳守)
- 全メソッドを
asyncで定義。中身が同期でも例外なく - 戻り値は
{ ok: true; data: T } | { ok: false; error: string }に統一 - コンポーネントから Zustand ストアを直接参照しない
navigator.geolocationをlib/location/gps-provider.ts以外で呼ばない(FR-111)lib/geo/は純粋関数のみ。 ストア・DOM・地図ライブラリに依存させない。ここが最もテストしやすく、最もバグが出る場所- 位置取得・地図操作に擬似ディレイを入れない
9. デザイン要件
アートディレクション
「地図の上に、必要なものだけ浮かせる」
FIELDPIN が使われるのは、屋外・立ったまま・片手・手袋・直射日光の下。オフィスのUIをそのまま持ち込むと、現場では使えない。
- 地図が全画面。 UIは地図の上に浮くカードとボトムシートだけ
- タップ領域を大きく。 最小56px。RELATE(業務PC向け高密度)とは正反対の方向
- 親指の届く範囲に主要操作を置く。 画面上部に重要ボタンを置かない
- 直射日光を想定する。 中間グレーを避け、コントラストを強く取る
- 色は状態のためにある。 訪問先の状態は色と形の両方で示す
姉妹プロジェクトとの差別化(重要) HIREBASE:明朝・白・広い余白(読ませる)/ RELATE:ゴシック・白・13px高密度(操作させる)/ CASTA:ダーク・映像優先(見せる)/ FIELDPIN:地図全画面・16px大型UI・56pxタップ領域(現場で使わせる) 4つ並べたとき、別の会社が作ったように見えることが理想。トークンをコピーしない。
カラートークン
:root {
/* 地図の上に乗るので、UIは白と濃紺のカードで固める */
--ink-900: #101418; /* 見出し・本文 */
--ink-600: #4B5563; /* 補助テキスト。★中間グレーはここまで */
--panel: #FFFFFF; /* カード・シート背景 */
--panel-alt: #F1F3F5; /* シート内の区切り */
--bar: #101822; /* 上端の状態バー・デモ切替バー */
--primary: #0B5FA5; /* 主要CTA・現在地マーカー */
--primary-d: #084river; /* ※実装時は #08477C に置換 */
/* 訪問先の状態。★色 + 形の両方で区別する */
--st-todo: #6B7280; /* 未訪問 … 丸 */
--st-active: #0B5FA5; /* 訪問中 … 二重丸 */
--st-done: #1F7A4D; /* 完了 … チェック */
--st-skip: #8A6D3B; /* スキップ… 斜線 */
--st-late: #B4281E; /* 遅延 … 三角 */
--geofence: rgba(11,95,165,.12); /* 圏外時の塗り */
--geofence-in: rgba(11,95,165,.28); /* ★圏内に入ると濃くなる */
--accuracy: rgba(11,95,165,.16); /* 精度円 */
--accuracy-bad: rgba(180,40,30,.18); /* 精度が悪いとき */
--offline: #B4281E; /* オフラインバナー */
--pending: #8A6D3B; /* 未同期バッジ */
}
配色ルール
- 地図と喧嘩する色を使わない。 彩度の高い緑・茶は地図の地物と混ざるため、UIでは使わない
- 中間グレーの文字を使わない。 屋外で読めなくなる。補助テキストも
--ink-600までとし、それより薄い色を作らない - 訪問先の状態は必ず色と形の両方で区別する(A11Y-04)
- カードは影ではなく白背景 + 1pxの縁で地図から浮かせる。影だけでは日光下で境界が消える
タイポグラフィ
| 役割 | 書体 | 用途 |
|---|---|---|
| UI全般 | Noto Sans JP 500 / 700 | 既定ウェイトを500にする。 屋外では400は細すぎる |
| 数値 | Roboto Mono 500(tabular-nums) |
距離、時刻、滞在時間、精度 |
| トークン | スマホ | PC(管理画面) | 用途 |
|---|---|---|---|
| title | 20px / 700 | 22px | 画面・シートのタイトル |
| item | 16px / 700 | 15px | 訪問先名、リスト項目 |
| body | 15px / 500 | 14px | 説明、報告本文 |
| meta | 13px / 500 | 13px | 住所、時刻 |
| num | 15px / 500 Mono | 14px | 距離・精度 |
| button | 16px / 700 | 15px | ボタンラベル |
モバイルの方が文字が大きい。 これは意図的。現場はスマホ、管理はPCという使い分けを前提にしている。
レイアウトとスペーシング
| 項目 | 定義 |
|---|---|
| スペーシング | 4pxベース:4 / 8 / 12 / 16 / 24 / 32 / 48 |
| 地図 | 全画面(100dvh)。 100vh を使わない(モバイルのURLバーで破綻するため) |
| セーフエリア | env(safe-area-inset-*) を必ず考慮すること |
| ボトムシート | 3段階:畳む(96px) / 中(45dvh) / 全開(88dvh)。ドラッグとタップで切替 |
| タップ領域 | 最小56×56px。 主要アクションは64px |
| 主要操作の位置 | 画面下1/3に配置。 上部には状態表示のみ |
| FAB | 右下、64px。「現在地へ戻る」は右下、その上に配置 |
| 角丸 | 12px(カード・シート)/8px(ボタン)/999px(チップ・FAB) |
| 縁 | カードは 1px rgba(16,20,24,.10) + 影 0 2px 8px rgba(16,20,24,.12) |
主要コンポーネント仕様
| コンポーネント | 仕様 |
|---|---|
| 現在地マーカー | 中心の点 + 精度円。進行方向が取れる場合は扇形を重ねる。精度が悪いときは円を警告色に(FR-207) |
| 訪問先ピン | 状態別の色と形。訪問中は脈動アニメーション。ラベルはズーム14以上で表示 |
| ジオフェンス円 | 通常は薄い塗り、圏内に入ると濃くなる(FR-204)。半径をメートルで表示 |
| ボトムシート | 3段階。ドラッグハンドルを明示。シートを引き上げても地図の現在地が隠れないよう、地図側を自動でオフセットする |
| 訪問先カード | 名称(item)/住所(meta)/距離(num・大きく)/予定時刻/状態バッジ/チェックインボタン |
| チェックインボタン | 幅いっぱい・高さ64px。圏内は --primary 塗り、圏外は枠線 + 「離れた場所から記録」 |
| 距離表示 | 1km未満はm単位(整数)、以上はkm単位(小数1桁)。等幅数字で桁を揃える |
| 状態バー(上端) | オフライン/未同期件数/位置モード/精度。該当がなければ表示しない(常時占有しない) |
| オフラインバナー | 上端固定、--offline。「オフラインです。記録は端末に保存されます」 |
| 同期進捗 | バナー内にプログレス。「同期中 2/3」→「同期しました」で3秒後に消える |
| 権限案内 | 全画面。ブラウザ別の手順をスクリーンショット付きで示す。下部にシミュレーション導線 |
| 軌跡再生コントロール | 再生/一時停止/速度(1x/4x/16x)/シークバー。時刻を等幅数字で表示 |
| 稼働マップのスタッフピン | アバター + 進捗バッジ(3/8)+ 最終更新(「3分前」)。5分以上前は半透明にする |
モーション
| 対象 | duration | 内容 |
|---|---|---|
| 地図の移動・ズーム | 400ms | ease-out。「現在地へ戻る」時のみ |
| ボトムシートのスナップ | 260ms | cubic-bezier(.32,.72,0,1) |
| ジオフェンス進入バナー | 240ms | 上から。同時にバイブレーション(対応端末のみ) |
| 訪問中ピンの脈動 | 2000ms | ループ。これだけは常時アニメーション |
| 現在地マーカーの移動 | 位置更新間隔に追従 | 補間して滑らかに動かす。 瞬間移動させない |
| チェックイン完了 | 300ms | ピンの色変化 + 軽いスケール |
prefers-reduced-motion: reduce 時は、脈動と補間を停止し、位置は瞬時に更新する。
アクセシビリティ(WCAG 2.1 AA)
| ID | 要件 |
|---|---|
| A11Y-01 | コントラスト比は通常4.5:1以上。屋外利用を考慮し、主要テキストは7:1を目標とする |
| A11Y-02 | 地図に依存しない操作経路を必ず用意すること。訪問先リストからすべての操作ができること(地図が見えなくても業務が回る) |
| A11Y-03 | タップ領域は最小56×56px |
| A11Y-04 | 訪問先の状態を色のみで伝えないこと。形・アイコン・テキストを併記 |
| A11Y-05 | 地図の各ピンにアクセシブルな名前(訪問先名 + 状態 + 距離)を持たせること |
| A11Y-06 | ジオフェンス進入を aria-live="assertive" で通知すること |
| A11Y-07 | オフライン・同期状態の変化を aria-live="polite" で通知すること |
| A11Y-08 | フォーカスリングは3px・オフセット2pxで常時可視。地図の上でも視認できる色にすること |
| A11Y-09 | ボトムシートはフォーカストラップせず、キーボードで開閉・スクロールできること |
| A11Y-10 | フォントサイズ200%指定でも、チェックインボタンとリストが操作できること |
| A11Y-11 | 位置情報を使わない選択肢を必ず提示すること。 権限を拒否した人が締め出されないこと |
ライティング規約
| 原則 | 例 |
|---|---|
| 権限は目的を先に言う | ○「訪問先への到着を自動で記録するために、位置情報を使います。位置情報は端末内にのみ保存され、どこにも送信されません」/×「位置情報の使用を許可してください」 |
| 拒否を責めない | ○「位置情報を使わずに試すこともできます」/×「位置情報が許可されていないため利用できません」 |
| 精度は正直に書く | ○「位置の精度が低い状態です(±180m)。屋外に出ると改善することがあります」/×「取得に失敗しました」 |
| 離れた記録を責めない | ○「訪問先から120m離れた場所で記録します。理由を選んでください」/×「不正な位置です」 |
| オフラインは不安にさせない | ○「オフラインです。記録は端末に保存され、接続が戻ると自動で同期されます」/×「通信エラー」 |
| できないことは明言する | ○「アプリを閉じている間は位置を記録しません」/× 黙って動かない |
| システム語を使わない | ○「訪問を記録する」/×「ステータスをin_progressに更新」 |
10. 技術要件・ディレクトリ構成
技術スタック(固定・勝手に変更しない)
| レイヤ | 技術 | 備考 |
|---|---|---|
| フレームワーク | Next.js 15(App Router)/ TypeScript strict | |
| スタイリング | Tailwind CSS + CSS Variables | |
| UIコンポーネント | shadcn/ui(Radix UI基盤) | タップ領域を拡大して上書き |
| 状態管理 | Zustand + persist | localStorage に永続化 |
| 地図描画 | MapLibre GL JS | オープンソース。特定ベンダーにロックインされない構成であることが記事の価値(12章) |
| 地図タイル | 12章で選定 | ベクタータイル。選定理由を記事に書けるようにすること |
| 位置取得 | Geolocation API(lib/location/ 経由のみ) |
|
| 幾何計算 | 自前実装(lib/geo/) |
Turf.js を入れない。Haversine と Douglas-Peucker は数十行で書け、自前で書けることを示す方がデモとして価値がある |
| ボトムシート | vaul(または自前実装) | 3段階スナップが必要 |
| グラフ | Recharts | 管理画面(動的import) |
| フォーム | React Hook Form + Zod | |
| 画像圧縮 | browser-image-compression | FR-408 |
| PWA | next-pwa(または自前 Service Worker) | オフライン起動 |
| 日付 | date-fns(ja locale) | |
| アイコン | lucide-react | |
| ホスティング | Vercel(HTTPS必須) | Geolocation は HTTPS でしか動かない |
| テスト | Vitest / Playwright / axe-core |
上記以外のライブラリを入れる前に必ず提案し、承認を得ること。特に Turf.js と Google Maps JS API の導入は禁止。
ディレクトリ構成
/
├── CLAUDE.md
├── app/
│ ├── layout.tsx
│ ├── page.tsx # SC-001 モード選択 / SC-090(?embed=1)
│ ├── permission/ # SC-002, 003
│ ├── map/ # SC-010〜016(★ 地図インスタンスはここで1つだけ保持)
│ ├── visits/[id]/report/ # SC-014
│ ├── history/ # SC-020, 021
│ ├── settings/ # SC-030, 031
│ ├── admin/ # SC-100〜160
│ └── dev/components/
├── components/
│ ├── map/ # ★ 最重要
│ │ ├── MapCanvas.tsx # MapLibre のライフサイクル管理(生成は1回だけ)
│ │ ├── CurrentLocationMarker.tsx
│ │ ├── PlaceMarker.tsx
│ │ ├── GeofenceCircle.tsx
│ │ ├── TrackLine.tsx
│ │ ├── TrackPlayer.tsx
│ │ └── useMapInstance.ts / useFitBounds.ts
│ ├── sheet/ # BottomSheet, PlaceCard, ScheduleList
│ ├── domain/ # CheckInButton, DistanceLabel, AccuracyBadge, OfflineBanner, SyncBanner, PermissionGuide
│ ├── demo/ # RoleSwitcher, LocationModeSwitcher, ResetButton, GuideTour
│ └── layout/
├── lib/
│ ├── types/ seed/ store/ repo/
│ ├── location/ # 8章参照。★ navigator.geolocation を呼ぶ唯一の場所
│ ├── geo/ # ★ 純粋関数のみ。テスト必須
│ ├── query/ validation/ utils/
├── e2e/
└── public/
コーディング規約
- TypeScript strict。
any禁止 navigator.geolocationをlib/location/gps-provider.ts以外で呼ばない(FR-111)lib/geo/は純粋関数のみ。 ストア・DOM・MapLibre に依存させない- MapLibre のインスタンスは1つだけ生成し、画面遷移で破棄・再生成しない(FR-210・費用要件)
watchPositionの ID を必ずclearWatchすること。 アンマウント時・モード切替時・visibilitychange時- MapLibre のマーカー・ソース・レイヤーも必ず破棄する。このアプリで最も起きやすい不具合はリスナーとマーカーのリーク
- 高さは
100dvhを使う。100vhを使わない - コミットは1タスクごと。メッセージに要件IDを含める
例:
feat(geofence): 進入退出判定にヒステリシスを追加 (FR-303)
11. 非機能要件・プライバシー
性能要件
| ID | 要件 | 目標値 |
|---|---|---|
| NFR-01 | 初回の位置取得(概位置)まで | 3秒以下 |
| NFR-02 | 地図の初期描画完了まで | 2.0秒以下 |
| NFR-03 | LCP(モバイル4G相当) | 2.5秒以下 |
| NFR-04 | INP | 200ms以下 |
| NFR-05 | 地図のパン・ズーム時のフレームレート | 60fps維持 |
| NFR-06 | 訪問先60件表示時も操作がもたつかないこと | — |
| NFR-07 | 埋め込みモードの初期JSバンドル(gzip後) | 60KB以下 |
| NFR-08 | アプリ本体の初期JSバンドル(gzip後) | 200KB以下(MapLibre・Recharts は動的import) |
| NFR-09 | 30分の連続稼働でメモリが単調増加しないこと | — |
| NFR-10 | Lighthouse | Performance 85以上 / Accessibility 95以上 ※地図ライブラリの重量を考慮 |
| NFR-11 | 対応環境 | iOS Safari 16以降、Android Chrome 最新、PC Chrome / Edge / Safari / Firefox 最新2バージョン |
| NFR-12 | localStorage 上限接近時に警告し、古い軌跡から削除すること。データを破損させないこと | — |
プライバシー(この領域固有の最重要要件)
位置情報は、日本の個人情報保護法において個人関連情報にあたり、他の情報と結びつくことで個人データとなる。扱いを間違えると事故になる領域であり、その配慮を見せることがデモの信頼性を作る。
| ID | 要件 |
|---|---|
| PRV-01 | 取得した座標を一切外部に送信しないこと。 localStorage にのみ保存すること |
| PRV-02 | この事実を、権限要求画面・設定画面・デモ切替バーの3箇所で明示すること |
| PRV-03 | GA4 に座標を含むイベントを送らないこと。 送るのは geo_permission_result のような状態のみ |
| PRV-04 | 「デモをリセット」ですべての位置データが消えることを明示し、実際に消えること |
| PRV-05 | 保存されている位置データの件数と容量を、設定画面でユーザーが確認できること |
| PRV-06 | 位置データを個別・一括で削除できる導線を、設定画面に置くこと |
| PRV-07 | 軌跡記録を OFF にでき、OFF 時は一切記録しないこと |
| PRV-08 | 管理者設定の「取得目的の明示文」が、スタッフ側の設定画面にも表示されること(FR-613) |
| PRV-09 | 写真の Exif 位置情報を読み取らないこと(FR-409) |
実案件への転用時の注意(デモ本体の要件ではない。商談で聞かれた際に説明できるよう記録)
- 従業員の位置情報を取得する場合、利用目的の特定と本人への明示が必要。就業規則や労使間の合意も論点になる
- 勤務時間外の位置取得は行わないことが原則。取得時間帯の制限を仕様に入れるべき
- 位置情報の保存期間を定め、期間経過後に削除する仕組みが必要
- 委託先(クラウド事業者)の管理、越境移転の確認が必要になる場合がある
- 本デモは位置情報をサーバーに送信しないため、これらの義務は発生しない。 ただし「デモである」旨は画面上に常時明示する
12. 費用設計
記事の主題の半分は「費用」。この章は実装要件であると同時に、記事本文の原稿素材でもある。
位置情報アプリの費用は「3つの分岐」で決まる
金額の前に、どの分岐を選ぶかで桁が変わることを説明する。ここが記事の核心。
分岐①:Webで足りるか、ネイティブが必要か(最大の分岐)
| やりたいこと | Web(PWA) | ネイティブ |
|---|---|---|
| 画面を開いている間の位置取得 | できる | できる |
| 訪問先への到着を、開いている間に検知 | できる | できる |
| オフラインでの記録と後からの同期 | できる | できる |
| ホーム画面から起動 | できる | できる |
| アプリを閉じている間の位置追跡 | できない | できる |
| バックグラウンドでのジオフェンス通知 | できない | できる |
| 常時の軌跡記録(1日中) | できない | できる |
ここを誤解したまま進めると、作った後に「使えない」となる。 逆に、「スタッフが訪問先で開く」運用が成立するなら、Webで十分であり費用は大きく下がる。
判断の目安: 記録のタイミングを人が能動的に行う業務ならWeb。気づかないうちに記録したい業務ならネイティブ。
分岐②:地図タイルの課金モデル
地図は無料ではない。そして課金の単位がサービスごとに違う。
| 課金モデル | 単位 | 特徴 |
|---|---|---|
| 地図読み込み課金 | 地図を1回初期化するごと | 画面遷移のたびに地図を作り直す実装だと、コストが数倍になる |
| MAU課金 | 月間アクティブユーザー数 | 使い込むユーザーが多くても増えない。ユーザー数が読めるなら有利 |
| タイルリクエスト課金 | タイル取得回数 | ズーム・パンが多いと増える |
| セルフホスト | サーバー費用のみ | 初期構築の手間はかかるが、使用量に比例しない |
地図読み込み課金の月額 = 月間の地図初期化回数 × 単価
月間の地図初期化回数 = MAU × 月間セッション数 × セッションあたりの初期化回数
例:スタッフ20名 × 月20日稼働 × 1日3セッション × 1回 = 1,200回/月
↑ ただし「画面遷移のたびに地図を作り直す」実装だと 1回→8回 になり、9,600回/月
FR-210(地図インスタンスを1つだけ保持する)は、UI要件ではなく費用要件。 この1点で費用が数倍変わることを、記事で具体的に示す。
分岐③:位置データの保存と分析
軌跡を1日中記録すると、1人あたり1日 数千点になる。これをサーバーに保存し、後から検索・集計するなら、PostGIS等の空間データベースと、それを扱える設計が必要になる。開発費と運用費の両方が上がる。
「訪問した/しなかった」だけを記録するなら、データ量は劇的に小さくなる。 何をどこまで記録するかを最初に決めることが、費用管理そのものになる。
3層のコスト構造
| 層 | 内容 | 特徴 |
|---|---|---|
| ① 初期開発費 | 設計・実装・テスト | 分岐①で大きく変わる(ネイティブは複数OS分) |
| ② 固定運用費 | ホスティング、ドメイン、監視、(ネイティブなら)ストア年会費 | 小さいが、ネイティブだと審査対応の工数が乗る |
| ③ 変動運用費 | 地図タイル、位置データの保存 | 分岐②③で決まる。設計次第で数倍変わる |
開発費の目安(スコープ別)
| スコープ | 含まれるもの | 目安 |
|---|---|---|
| 最小構成 | 地図表示 + 現在地表示 + 手動チェックイン | 小 |
| 標準構成(本デモ相当) | 権限フロー/精度処理/ジオフェンス/訪問報告/オフライン記録/管理画面/軌跡再生 | 中 |
| 拡張構成 | 上記 + ネイティブアプリ(iOS/Android)+ バックグラウンド追跡 + 空間DB | 大 |
「位置情報アプリ」という言葉の幅が広いことが、費用が伝わらない最大の原因。 記事ではこの3段階を明示し、デモは「標準構成」の実物であることを示す。具体的な金額はLPの価格体系に接続する。
記事に載せる際の注意: 各地図サービスの単価は変動するため、この文書に金額を書き込まない。 記事執筆時点で公式の料金ページを確認し、確認日を明記して掲載すること。 無料枠の条件(クレジット付与か、完全無料か)も併記すること。
デモ自体の運用コストを実測して記事に載せる
| ID | 要件 |
|---|---|
| COST-01 | デモ公開後、月次で「デモ起動数・地図初期化回数・タイル取得数・実際にかかった費用」を記録すること |
| COST-02 | 記録した実測値を記事に掲載し、確認日を明記すること |
| COST-03 | 地図サービスの使用量アラートを設定すること。記事がバズった場合に費用が跳ねるリスクへの備え |
| COST-04 | EMB-02(静止画プレビュー → 押下で地図を読み込む)により、記事を読んだだけでは地図が読み込まれない設計になっていることを、記事内で費用対策の実例として説明すること |
「記事に地図を埋め込むと、読まれるたびに課金される。だから押されるまで読み込まない設計にした」——この一文が、費用を理解して設計できることの証明になる。
13. 実装タスク
この順に進める。フェーズを飛ばさない。 完了時は [x] に更新する。
Phase 0 — 基盤と位置の抽象化(3日)
0-1. 初期化
- Next.js 15 / TypeScript strict / App Router / Tailwind で初期化
- ESLint / Prettier / husky(pre-commit で lint + typecheck)
- 10章のディレクトリ構成を作成
- ローカル開発をHTTPSで行えるようにする(Geolocation の検証に必須)
0-2. lib/geo/(純粋関数。最初に書いてテストで固める)
-
distance.tsHaversine 距離 -
geofence.ts進入・退出判定(ヒステリシス付き、FR-303) -
simplify.tsDouglas-Peucker(DATA-01) -
bounds.ts表示範囲の算出(FR-206) -
nearest.ts最近傍法による並び替え(FR-502) - 上記すべての単体テストを書く。ここが最もバグが出る場所
0-3. lib/location/(位置の抽象化。3章の中核)
-
provider.tsLocationProviderインターフェース定義 -
gps-provider.ts—navigator.geolocationを呼ぶ唯一の場所(FR-111) - 2段階取得(低精度→高精度)(FR-103)
-
visibilitychangeでの停止・再開 (FR-112) -
clearWatchの確実な実行 -
simulation-provider.ts— ルート走行、精度の揺らぎと誤差を再現(FR-152) -
manual-provider.ts— 地図タップで現在地を指定(FR-154) -
permission.ts— Permissions API のラッパー、状態判定(FR-106) -
filter.ts— ドリフト棄却(速度閾値)・精度フィルタ(FR-105) - Provider の単体テスト(シミュレーションで決定的に検証)
0-4. 型とシード
-
lib/types/に8章の型定義をすべて実装 -
lib/seed/の全ファイル - 座標を道路上・建物上に自然に配置する。海の上にピンを立てない
- 軌跡を道なりにする。直線で結ばない
- 半径外チェックインを数件、遅延スタッフを1名、訪問中の予定を1件含める
- 実在住所・実在個人を指すデータを作らない
0-5. ストアとリポジトリ
-
lib/store/{session,data,settings,sync,ui}.tsを Zustand + persist で実装 -
lib/repo/_scope.ts(FR-708) -
lib/repo/の全モジュール。位置取得・地図操作にディレイを入れない - 軌跡の間引きと保持上限(DATA-01〜04)
0-6. デザインシステム
-
app/globals.cssにカラートークンを CSS 変数で定義 -
tailwind.config.tsを拡張。タップ領域56pxを既定にする - フォント(Noto Sans JP 500/700 / Roboto Mono)。距離・時刻に
tabular-nums -
100dvhとセーフエリアの対応 - shadcn/ui を導入し、タップ領域を拡大して上書き
- 共通コンポーネント:
BottomSheet(3段階スナップ)/DistanceLabel/AccuracyBadge/StatusPin/EmptyState/Skeleton -
prefers-reduced-motionの対応 -
/dev/componentsにコンポーネントカタログを作成
0-7. デモ基盤
-
RoleSwitcher(3ロール)/LocationModeSwitcher(3モード + 精度表示)(FR-701, FR-702) - デモ切替バー(地図に重ならない上端固定の濃色帯)
- 「デモをリセット」(FR-703)
- 元記事リンク・資料ダウンロード (FR-706)
Phase 0 完了チェック
-
lib/geo/のテストが全パターン通る - シミュレーションProviderで、位置が滑らかに更新される
- 実機(スマホ・HTTPS)で
gps-providerが実際の座標を返す -
/dev/componentsで全コンポーネントがトークン通りに表示される
Phase 1 — 地図と権限フロー(5日・最重要フェーズ)
1-1. 地図基盤
- MapLibre GL JS の導入(動的import)
- タイルサービスの選定と設定(12章の判断を記事に書けるよう、選定理由を記録すること)
-
MapCanvas.tsx:インスタンスを1つだけ生成し、画面遷移で破棄しない(FR-210) -
useMapInstance.ts:ライフサイクル管理。マーカー・ソース・レイヤーの確実な破棄 -
CurrentLocationMarker.tsx:点 + 精度円 + 進行方向の扇形 (FR-104, FR-110) - 位置更新時の補間移動(瞬間移動させない)
- 精度が悪いときの警告表示 (FR-207)
-
PlaceMarker.tsx:状態別の色と形(FR-202)。訪問中は脈動 -
GeofenceCircle.tsx:圏内で塗りが濃くなる(FR-204) - クラスタリング (FR-203)
- 「現在地へ戻る」FAB (FR-205)
- 初期表示範囲の自動調整 (FR-206)
- 地図スタイル切替(標準/航空写真風/高コントラスト)(FR-209)
1-2. 権限フロー(ここの丁寧さがこのデモの価値)
- SC-001 モード選択(起動直後に権限プロンプトを出さない)(FR-101)
- SC-002 権限リクエスト(目的と非送信の説明を先に)(FR-102)
- SC-003 拒否・ブロック時の案内:ブラウザ別の解除手順(iOS Safari / Android Chrome / PC Chrome / Firefox)(FR-106)
- シミュレーションへの導線を必ず併記 (A11Y-11)
- 非対応・非HTTPS の検知と案内 (FR-107)
- タイムアウト処理 (FR-108)
- 権限の全パターン(prompt / granted / denied / unsupported / insecure)を手動で検証する
1-3. メインマップ
- SC-010 メインマップ(全画面地図 + ボトムシート + FAB)
- SC-011 今日の予定シート(3段階スナップ、地図と連動)
- シート引き上げ時に現在地が隠れないよう地図をオフセット
- SC-012 訪問先詳細シート(距離を大きく表示)
- 現在地近くに仮の訪問先を自動生成(FR-705。どの地域で開いても成立させる)
- 位置モードの常時表示 (FR-156)
- 取得間隔の設定 (FR-109)
1-4. 埋め込みモード
- SC-090 埋め込みモード:Geolocation を一切呼ばない(FR-151, EMB-02)
- 静止画プレビュー + 「デモを再生」→ 押下で地図ライブラリを動的import
- シミュレーション自動走行・ループ、タブ非表示で停止 (EMB-05)
- 初期バンドル 60KB以下を実測 (EMB-03, NFR-07)
- アプリ内遷移の禁止、「実際に試す」は
target="_blank"
Phase 1 完了チェック
- 実機で自分の現在地が地図に表示される
- 権限を拒否しても、シミュレーションで全機能が使える
- 画面を遷移しても地図インスタンスが再生成されない(FR-210 を DevTools で確認)
- 埋め込みモードで権限プロンプトが一切出ない
- 初回の概位置取得が3秒以下(NFR-01)
Phase 2 — ジオフェンスと訪問記録(4日)
- 位置更新ごとのジオフェンス判定(Haversine + ヒステリシス)(FR-302, FR-303)
- 進入通知:バナー + 通知センター + バイブレーション (FR-304)
-
aria-live="assertive"での通知 (A11Y-06) - SC-013 チェックイン確認(距離・精度の表示)(FR-305)
- 半径外チェックインの理由入力必須化(ブロックはしない)(FR-306)
- 二重チェックインの防止 (FR-308)
- SC-015 チェックアウト(滞在時間、報告未記入の警告)(FR-307)
- 訪問のスキップと理由記録 (FR-309)
- SC-014 訪問報告(テンプレート + 自由記述 + 申し送り)(FR-401)
- 下書きの自動保存 (FR-402)
- 写真添付と撮影時点の位置紐付け (FR-407)
- 画像圧縮 (FR-408)、Exif を読まない (FR-409)
- SC-016 訪問順の最適化(適用前後の総距離比較、自動適用しない)(FR-502, FR-503)
- 外部地図アプリでのルート案内 (FR-504)
- 軌跡の記録と総移動距離の算出 (FR-208, FR-505)
- SC-020 / SC-021 活動履歴
Phase 2 完了チェック
- フローBが完走する(ジオフェンス進入 → チェックイン → 報告 → チェックアウト)
- 境界を行き来してもジオフェンスが連続発火しない
- 手動モードで地図をタップしながら、ジオフェンスの進入・退出を再現できる
Phase 3 — オフラインとPWA(3日)
- Service Worker / PWA マニフェスト(F-S06)
- オフライン検知とバナー (FR-406)
- オフライン時のチェックイン・報告・写真添付 (FR-403)
- 同期キューの実装と「未同期」バッジ
- オンライン復帰の自動同期と進捗バナー (FR-404)
- SC-031 未同期キュー(一覧・手動同期・個別削除)(FR-405)
-
aria-live="polite"での状態通知 (A11Y-07) - SC-030 設定(位置モード、取得間隔、通知、軌跡記録ON/OFF)
- 保存データの件数・容量の可視化と削除導線(PRV-05, PRV-06)
- localStorage 上限接近時の処理 (NFR-12)
Phase 3 完了チェック
- フローCが完走する(オフラインで記録 → 復帰 → 自動同期)
- 機内モードでアプリを起動でき、記録ができる
Phase 4 — 管理画面(4日)
- SC-100 稼働マップ(全スタッフの現在地、最終更新からの経過時間、5分以上前は半透明)(FR-601)
- 進捗バッジと遅延アラート (FR-602)
- SC-101 / SC-102 スタッフ一覧・詳細
- SC-110 軌跡再生(日付・スタッフ選択、タイムライン再生、速度変更、シーク)(FR-603)
- SC-120 訪問先マスタ(一覧 + 地図の2ペイン、ピンのドラッグで座標修正)(FR-604)
- SC-121 ジオフェンス半径を地図上で円をドラッグして設定 (FR-605)
- SC-130 訪問予定(割当、日付・時間帯、繰り返し)(FR-606)
- SC-140 訪問記録一覧(距離の列表示、離れているものの絞り込み)(FR-607)
- SC-141 訪問記録詳細(チェックイン地点と訪問先の位置関係を地図で可視化)(FR-608)
- SC-150 実績ダッシュボード (FR-609)
- SC-151 エリア分析ヒートマップ (FR-610)
- 異常検知 (FR-611)
- CSVエクスポート (FR-612)
- SC-160 位置情報の設定(取得間隔・保存期間・軌跡記録・取得目的の明示文)(FR-613, PRV-08)
Phase 4 完了チェック
- 5章「ロールとモードを跨ぐ体験」の5パターンがすべて動作する
- 軌跡再生が滑らかで、道なりに動いて見える
Phase 5 — 記事統合と仕上げ(3日)
5-1. 記事統合
- 記事側の埋め込みコードを作成し、実際の記事ページで動作確認 (EMB-01〜05)
- 埋め込みが記事のCore Web Vitalsを悪化させないことを、埋め込み前後で比較検証
- 「実際に自分の現在地で試す」カードの設置
- 資料PDF(判断表・費用計算シート)の作成とダウンロード導線 (F-S03)
- 元記事リンク・問い合わせ導線 (EMB-10, EMB-11)
- GA4 イベント設定。
geo_permission_resultを必ず含める(EMB-12, EMB-14) - GA4 に座標を送っていないことを確認 (PRV-03)
-
?embed=1をnoindexに (EMB-21) - デモ本体の title / description / OGP (EMB-20)
- 記事側に
VideoObjectとHowToの構造化データ (EMB-23)
5-2. 体験の総点検
- 5章の5パターンをすべて手動で確認
- 全26画面を開き、空の画面が1つもないことを確認
- 実機(iOS Safari / Android Chrome)での実地テスト。 実際に外を歩いて検証すること
- 屋内・地下での挙動を確認(精度低下時の表示)
- ガイドツアーを実装 (FR-709)
5-3. アクセシビリティ監査
- axe-core を全画面に実行し、Critical / Serious を0件に
- 地図を使わずに(リストだけで)全業務が完結することを確認 (A11Y-02)
- キーボードのみで全画面を操作完遂
- タップ領域56px以上を全画面で確認 (A11Y-03)
- フォントサイズ200%での操作確認 (A11Y-10)
- 屋外(直射日光下)での視認性を実機で確認
5-4. パフォーマンス
- MapLibre / Recharts を動的import に切り出し、初期バンドルを200KB以下に (NFR-08)
- 初回の概位置取得3秒以下を実測 (NFR-01)
- 地図操作の60fpsを実測 (NFR-05)
- 30分連続稼働でのメモリ推移を計測(watchPosition とマーカーのリーク検証)(NFR-09)
- Lighthouse で Performance 85 / Accessibility 95 (NFR-10)
5-5. 公開と記録
- Playwright で フローA・B・C の E2E(シミュレーションProviderを使って決定的に)
- Vitest:
lib/geolib/location_scopeのカバレッジ80%以上 -
/dev/componentsを本番で非公開に - Vercel へデプロイ(HTTPS必須)、地図サービスの使用量アラートを設定 (COST-03)
- 地図初期化回数・タイル取得数・費用の記録を開始 (COST-01)
- 解説動画(3分)の収録:シミュレーション走行 → ジオフェンス → チェックイン → 管理画面での軌跡確認
- 各フェーズのキャプチャを整理し、記事の「方法」パートの素材にまとめる
合計 22営業日(約4.5週間)/1名専任 + レビュー体制。
工数配分の意図: Phase 0 の
lib/geo/とlib/location/に丸1日以上を割き、テストで固めてから先に進む。位置情報アプリのバグは、実機を持って外を歩かないと再現しない。 純粋関数として切り出してテストできる部分を最大化しておくことが、後半の工数を決める。 Phase 1 に5日を割いているのは、権限フローの丁寧さがこのデモの価値そのものだから。「許可してください」の一言で済ませるアプリとの差が、ここに出る。
14. 受入基準
- 7章の優先度 P1 の要件がすべて実装されていること
- 6章の主要フローA・B・Cが、エンドツーエンドで完走すること
- 5章「ロールとモードを跨ぐ体験」の5パターンがすべて動作すること
- 実機で、自分の実際の現在地が地図に表示されること
- 位置情報の権限を拒否しても、シミュレーションで全機能が体験できること
- 権限の全状態(prompt / granted / denied / unsupported / insecure)で、適切な案内が表示され、白画面や無言の失敗が起きないこと
- ジオフェンスの進入・退出が正しく判定され、境界で連続発火しないこと
- 半径外のチェックインが、ブロックされずに理由付きで記録できること
- オフラインで記録でき、復帰時に自動同期されること
- 画面遷移で地図インスタンスが再生成されないこと(DevTools で確認。費用要件)
- 埋め込みモードで Geolocation API が一度も呼ばれないこと、初期バンドルが60KB以下であること
- 取得した座標が外部に一切送信されていないこと(DevTools の Network で確認。GA4 のイベントにも含まれないこと)
- 30分連続稼働でメモリが単調増加しないこと
- 全26画面のいずれにも空の状態がなく、リアルなデータが表示されていること
- 地図を使わず、リストだけで全業務が完結できること
- axe-core で Critical / Serious の指摘が0件であること
- データアクセスがすべて
lib/repo/を経由し、navigator.geolocationがgps-provider.ts以外で呼ばれていないこと - HIREBASE / RELATE / CASTA と並べたときに、明確に別のプロダクトとして見えること
判断に迷ったときのルール
- 仕様がこの文書にない場合は、実装せずに質問する。 推測で作らない
- 権限フローの丁寧さ、位置を使わない選択肢、空の画面を作らないことは削減対象外
navigator.geolocationをlib/location/gps-provider.ts以外で呼ばないlib/geo/を純粋関数に保つ。 ここにストアやDOMを持ち込まない- 地図インスタンスを1つだけ保持する。 これはUI要件ではなく費用要件
watchPositionのclearWatchと、MapLibre のマーカー・リスナーの破棄を忘れない。このアプリで最も起きやすい不具合はリーク- スコープ外(4章 Won't have)は実装しない。特にバックグラウンド追跡は、Webでは不可能。 できないことを曖昧にしない
- 「デモだから」を理由に品質を落とす判断はしない
- HIREBASE / RELATE / CASTA のトークン・コンポーネントをコピーしない

