DEV LOG · 2026.05.13

訪問記録APP 開発記録

齋藤 雄三 — Ver1.x(ローカル)から Ver2.4(クラウド)まで
このドキュメントについて:訪問看護ステーション向けの訪問時間記録アプリを、Claude Codeと一緒に作った経緯をまとめたもの。 何をどういう順番で作って、どんな問題にぶつかって、どう解決したかの記録。 「なんでこういう設計になっとるんだっけ」となったときに戻ってくる用。

01プロジェクト概要

訪問看護師が1日5〜10件の訪問先でストップウォッチ的に滞在時間を記録するWebアプリ。タップ1回で開始・終了を記録し、日々のデータをクラウドに蓄積する。

作った背景

訪問看護の現場では「いつ・どこで・何分訪問したか」を正確に記録することが、診療報酬の根拠になる。既存の紙記録や汎用アプリでは不十分で、GPS情報つきの時刻記録を改ざん困難な形で残せる仕組みが必要だった。

将来的に診療報酬改定で「GPS記録による適正訪問の証明」が加算要件になる可能性も見据えて、今のうちからデータを蓄積しておく目的もある。

主な機能

最終的な技術スタック

フロントエンド素のHTML / CSS / JavaScript(フレームワークなし)
バックエンド / DBSupabase(PostgreSQL + Auth + Edge Functions)
ホスティングCloudflare Pages
ソースコード管理GitHub(Private)
メール通知Resend
開発ツールClaude Code
↑ 目次へ戻る

02Ver1.x — ローカル版(IndexedDB)

方針:まずローカルで動くものを作る

ネットワーク版から入るとSupabaseの設計・認証・RLSなどを一度に考える必要があって複雑になる。まず手元で動くものを作ってUIと操作感を確認する、という順序で進めた。

IndexedDBとは

ブラウザに内蔵されたローカルデータベース。サーバー不要でデータを保存できる。ページをリロードしてもデータが消えない。

// データ構造(ローカル版)
DailyRecord {
  date: "YYYY-MM-DD",   // Primary Key
  visits: Visit[],
  createdAt: string
}

Visit {
  id: crypto.randomUUID(),
  startTime: string | null,
  endTime: string | null,
  duration: number | null   // 秒
}

Ver1.xで実装したもの

ローカル版の限界

IndexedDBはそのブラウザ・その端末にしかデータがない。複数人での運用、端末の切り替え、データの共有ができない。ネットワーク版への移行が必要だった。

↑ 目次へ戻る

03Ver2.1 — ネットワーク版ローンチ・Chrome Macバグとの戦い

Supabase移行

IndexedDBからSupabase(PostgreSQL)へ移行。認証・RLS(行レベルセキュリティ)・リアルタイムDB・Edge Functionsが一体になったBaaSで、個人〜小規模チームには過不足なくちょうどいい。

Supabaseで作ったテーブル

テーブル内容
profiles職員情報(name, role, approved)
visits訪問記録(date, staff_id, visits JSON, memo等)
patients利用者マスタ(name, active)

Chrome Macでログインが無限ハングする問題

ネットワーク版を公開してすぐ、Chrome Mac環境でログイン後にアプリ画面が表示されない問題が発覚した。Safariでは動く。

原因の特定

DevToolsで直接fetchを叩いてみると即座にレスポンスが返ってくる。つまりネットワーク自体は問題ない。

// DevToolsで試したら即返ってきた → ネットワークは正常
fetch('https://xxx.supabase.co/rest/v1/profiles', {
  headers: { 'apikey': '...', 'Authorization': 'Bearer ...' }
})
// → OK 200

調査の結果、Supabase JS SDKが内部で使っているWeb Locks APInavigator.locks)がChrome Macで特定条件下で永久にブロックされることが原因と判明。

解決策その1:Web Locksをバイパス

const sb = createClient(SUPABASE_URL, SUPABASE_ANON, {
  auth: {
    lock: async (name, acquireTimeout, fn) => fn()  // ロック無効化
  }
});

これでログインは通るようになったが、今度はログイン後のデータ取得がハングした。SDKのINITIAL_SESSIONトークンリフレッシュ処理も同様にロックを使っていたため。

解決策その2:全データ操作をraw fetchに置き換え

SDKのデータ操作メソッドを全廃し、素のfetchでSupabase REST APIを直接叩くヘルパー関数を作った。

async function apiFetch(method, table, params = '', body = null, extraHeaders = {}) {
  const token = currentSession?.access_token;
  const headers = {
    'apikey': SUPABASE_ANON,
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    ...extraHeaders
  };
  if (token) headers['Authorization'] = `Bearer ${token}`;
  const url = `${SUPABASE_URL}/rest/v1/${table}${params ? '?' + params : ''}`;
  const opts = { method, headers };
  if (body !== null) opts.body = JSON.stringify(body);
  const res = await fetch(url, opts);
  if (res.status === 204 || res.status === 201) {
    const text = await res.text();
    return text ? JSON.parse(text) : null;
  }
  return res.json();
}
教訓

SDKは便利だが、内部実装に依存した罠がある。「動かない」と思ったらまずraw fetchで疎通確認。ネットワークが正常ならSDKの問題を疑う。

Ver2.1で実装したもの

↑ 目次へ戻る

04Ver2.2 — 機能大幅拡張

メモ機能 + 編集ダイアログ

各訪問カードにメモ欄を追加。また完了済み訪問の開始・終了時刻とメモを後から修正できる編集ダイアログを実装した。

設計の悩みどころ

「時刻の後修正を許すべきか」は議論があった。不正防止の観点では禁止したほうがいいが、入力ミスの修正ニーズもある。現状は編集可能にしつつ、将来の仕様検討事項としてTODOに残している。

利用者名入力のIME問題修正

日本語変換(IME)の確定前にEnterキーが反応してしまい、未確定の文字がそのまま入力される問題を修正。

// compositionstart / compositionend で変換中かどうかを追跡
input.addEventListener('compositionstart', () => { isComposing = true; });
input.addEventListener('compositionend',   () => { isComposing = false; });

input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' && !isComposing) commitLabel();
});

GPS取得を初回開始時のみに変更

以前は編集時にもGPSを再取得していた。記録の証拠性を高めるため、GPSは最初の「開始」ボタンを押した時のみ取得し、編集では上書きしない仕様に変更。

// 既にlocationが記録済みなら取得しない
if (!v.location) {
  navigator.geolocation.getCurrentPosition(pos => { ... });
}

履歴ビューの月別アコーディオン

以前は全記録がフラットに並んでいた。月ごとにグループ化し、クリックで開閉できるアコーディオン形式に変更。件数・合計時間もサマリー表示。

集計ビューの月別フィルター + ドリルダウン

集計ビューに「全期間 / 月別」フィルターを追加。各集計カードをクリックすると日別の詳細データが展開表示される。

CSV出力(GPS情報含む)

利用者別・職員別の集計データをCSVでエクスポート。ExcelでそのままUTF-8で開けるようBOM付きで出力。

CSVの種類含まれる項目
利用者別日付、曜日、職員名、利用者名、開始/終了時刻、時間、住所、緯度、経度、精度、メモ
職員別日付、曜日、職員名、訪問件数、合計時間、利用者名一覧
将来への備え GPS情報をCSVに含めておくのは、将来の診療報酬改定で「GPS記録による適正訪問の証明」が加算要件になる可能性に備えたもの。今は実験的にデータを蓄積しておく段階。
↑ 目次へ戻る

05Ver2.3 — Git / GitHub / 自動デプロイ環境を整える

Netlifyのクレジットが底をついた

それまでNetlifyのドラッグ&ドロップデプロイを使っていた。これが1回10クレジット消費する仕組みで、開発中に繰り返しデプロイするうちに無料枠(100クレジット)をほぼ使い切ってしまった。

根本的な解決策

ドラッグ&ドロップをやめてGitHubと連携した自動デプロイに切り替える。git pushするとNetlifyが自動でデプロイしてくれる仕組みで、この方式ならクレジットを消費しない。

Git初期化〜GitHub連携の手順

# Gitリポジトリを初期化
cd "/Users/yuzosaito/App Demo/訪看訪問時間管理"
git init
git add index.html app.js style.css CHANGELOG.md .gitignore
git commit -m "初回コミット: 訪問記録APP Ver2.2"

# GitHubリポジトリ(homebisit-app)を作成後
git remote add origin https://github.com/UzAvie/homebisit-app.git
git branch -M main
git push -u origin main

認証にはPersonal Access Tokenを使い、macOSキーチェーンに保存して以降は自動認証。

日常のデプロイフロー

# コードを修正したら
git add index.html app.js style.css
git commit -m "変更内容のメモ"
git push
# → GitHubに届いたのをNetlifyが検知して自動デプロイ(約4秒)
Git運用のポイント

コミットメッセージはCHANGELOGと連動させるとあとで追いやすい。git add -Aより特定ファイル名でgit addする方が意図しないファイルの混入を防げる。

TODO.mdもGit管理下に移行

それまでアプリフォルダの外に置いていたTODO.mdをリポジトリ内に移動。CHANGELOG.mdと並んで同じGitリポジトリで管理するようにした。

↑ 目次へ戻る

06Ver2.4 — メール通知 · Cloudflare Pages移行

新規登録時の管理者メール通知

職員が新規登録すると管理者に通知メールが届く仕組みを実装。それまでは管理者が定期的に⚙モーダルを開いて確認するしかなかった。

使ったサービス

サービス役割
Resendメール送信API(月3,000通まで無料)
Supabase Edge Functionsメール送信処理(Deno/TypeScript)
Supabase Database WebhooksprofilesテーブルへのINSERTを検知してFunctionを起動

全体の流れ

新規登録される
  → profiles テーブルに INSERT
    → Database Webhook が発火(on_new_profile)
      → Edge Function(notify-new-signup)が呼ばれる
        → Resend API でメール送信
          → 管理者のGmailに届く

Edge Functionのコード(要点)

Deno.serve(async (req) => {
  const payload = await req.json();
  const record = payload.record;
  const name = record?.name ?? "(名前未設定)";

  await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${Deno.env.get("RESEND_API_KEY")}`,
    },
    body: JSON.stringify({
      from: "onboarding@resend.dev",
      to: Deno.env.get("ADMIN_EMAIL"),
      subject: "【訪問記録APP】新規登録があります",
      html: `

${name} さんが登録申請しました。

アプリを開く

` }), }); return new Response(JSON.stringify({ ok: true }), { status: 200 }); });

APIキーや管理者メールアドレスは Supabase の Edge Function Secrets(環境変数)に保存し、コードには直書きしない。

Cloudflare Pagesへの移行

Netlifyのクレジットが残り30を切った(自動デプロイ移行後も初回設定で消費)ため、Cloudflare Pagesへ完全移行した。

Netlify(旧)Cloudflare Pages(新)
無料デプロイ数100クレジット(消耗品)500ビルド/月(毎月リセット)
帯域100GB/月無制限
GitHub連携
デプロイ速度約4秒同等

移行作業はGitHubリポジトリをそのまま使いまわせるため、Cloudflare Pagesの管理画面から homebisit-app を選んで接続するだけで完了した。コードの変更は不要。

新しいURL https://homebisit-app.pages.dev(旧URL: kaleidoscopic-kitten-0039d9.netlify.app)
↑ 目次へ戻る

07技術スタック全体像

各サービスの役割

サービス役割保存するもの
GitHubコード管理ソースコード・変更履歴
Cloudflare Pages公開・配信HTML / CSS / JS(コードのコピー)
Supabaseデータ管理・認証訪問記録・職員・利用者
Resendメール送信

全体のデータフロー

【開発時】
PCでコードを修正
  → git push
    → GitHubにコードが保存
      → Cloudflare Pagesが自動検知
        → デプロイ完了(数秒)

【利用時】
スマホ・PCでアプリを開く
  → Cloudflare PagesがHTML/JSを配信
    → ブラウザでアプリが動く
      → 操作のたびにSupabaseと通信
        → データが保存・取得される

コードとデータは独立している

コード(GitHub + Cloudflare)とデータ(Supabase)は別の場所にある。コードをいくら変えてもデータは消えないし、データが増えてもコードは変わらない。これがクラウドネイティブな設計の基本。

従来型レンタルサーバーとの違い

従来型はサーバー1台にHTML・DB・メールが全部同居していた。今の構成は役割ごとに専門サービスが分担する「モダン分散型」。初期費用ゼロ・メンテナンス不要・スケーラブルが特徴。小規模なら全部無料で動く。

↑ 目次へ戻る

08得た知見・今後のTODO

この開発で得た知見

① SDKを疑うことを覚えた

Chrome Macのハング問題は「ネットワークが悪い」と思いがちだが、原因はSDK内部のWeb Locks APIだった。raw fetchで疎通確認するという診断手順が身についた。

② バージョン管理は最初から入れるべき

Git導入はVer2.3からだったが、もっと早い段階から入れておけばよかった。git initのコストはゼロなので、プロジェクト開始時点でGitを入れるのが正解。

③ CHANGELOG + TODO を運用する習慣

何をどのバージョンで実装したかをCHANGELOG.mdに残し、次にやることをTODO.mdに整理する。これがないと「前回何やったっけ」が発生する。Claude Codeとの作業でも文脈が引き継ぎやすくなる。

④ Cloudflareは小規模アプリに最適

Netlifyは無料枠の制限が厳しくなっている。新規プロジェクトは最初からCloudflare Pagesにしておくほうが無難。

今後のTODO(主なもの)

カテゴリ内容
機能追加会議・外来同行・オンコール対応などの業務タイプを追加
機能追加利用者情報の拡張(対象/対象外フラグ、住まい、GAFスコア等)
仕様検討時刻の後修正を禁止する方向での検討(不正防止)
仕様検討管理者権限の多段階化(全権/副管理者/一般)
インフラ独自ドメイン設定
将来対応診療報酬改定を見据えたGPS証明の仕組み整備

リンク集

アプリURLhttps://homebisit-app.pages.dev
GitHubリポジトリhttps://github.com/UzAvie/homebisit-app
コード場所(ローカル)/Users/yuzosaito/App Demo/訪看訪問時間管理/
↑ 目次へ戻る