Skip to content

📱 003 クラウド同期・iPhone設計

画面構成・Service Worker・データ構造・データフロー

設計書 v1.4 / 2026-04-27


1 画面構成

iPhone PWA は step state で切り替わる5画面の単一ページアプリです。

1.1 各画面の役割

banner・login・push・list・write の5画面それぞれの表示条件と役割を説明します。

インストール案内画面 — Safariで /viewer を開いたとき(PWAではない)
ホーム画面に
追加してください
STEP 1 — 共有をタップ
STEP 2 — ホーム画面に追加
STEP 3 — 追加をタップ
app 2.9.x
banner
step: banner
表示条件
SafariでURLを直接開いた(PWAではない)
目的
ホーム画面への追加手順を案内する。初回インストール時のみ使う。
バージョン表示
app x.x.x のみ。ServiceWorkerは表示しない。
備考
skipWaiting導入済みのため、バージョンアップ目的で開く必要はない。
PWA画面 — ホーム画面アイコンから起動(URLバーなし・アプリとして動作)
俺の付箋
Googleでログイン
してください
Googleでログイン
app 2.9.x / ServiceWorker 2.9.x
ログイン
step: login
表示条件
アクセストークンがない(初回 or 有効期限切れ)
操作
「Googleでログイン」ボタン → Google OAuth(PKCE)→ トークン取得後 push へ遷移
バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)
セットアップ
プッシュ通知を
有効にしてください
通知を許可する
app 2.9.x / ServiceWorker 2.9.x
通知設定
step: push
表示条件
トークンあり + viewer_push_done が未設定
操作
「通知を許可する」→ OS権限ダイアログ → 許可後 list へ遷移。viewer_push_done=true を localStorage に保存。
バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)
メモ
大事なメモ🔔
買い物リスト🔕
アイデアメモ🔕
app 2.9.x / ServiceWorker 2.9.x
メモ一覧
step: list
表示条件
トークンあり + viewer_push_done=true(通常の起動時)
操作
+ → write(新規)
メモタップ → write(編集)
🔔 → ロック画面に表示 ON/OFF
🗑️ → メモ削除
バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)
← 戻るPCへ送る
大事なメモ...
iPhoneに置く
📷
B
H1
app 2.9.x / ServiceWorker 2.9.x
編集
step: write
表示条件
① list からメモ選択・新規作成
② 通知タップ(notificationclick または pending_open)
操作
「PCへ送る」→ Drive経由でPC送信 → list へ
「iPhoneに置く」→ IndexedDBに下書き保存 → list へ
「← 戻る」→ list へ(保存なし)
バージョン表示
app x.x.x / ServiceWorker x.x.x(右下固定)

1.2 起動時の遷移ルール

PWA起動時にどの画面から始まるかを決める4つのルールを定義します。

図 3-1 起動時の画面遷移ルール

表 1.2-1 起動時画面遷移ルール

No起動時の状態遷移先
1非 standalone(Safariで開いた)banner
2URL に ?code=(OAuthコールバック)Vercel でトークン取得 → push
3URL に ?note=(通知タップで起動)IndexedDB からノート読み込み → write
4token あり・pending_open あり(30分以内)IndexedDB からノート読み込み → write
5token あり・viewer_push_done=truelist(通常起動)
6token あり・push 未設定push(通知セットアップ)
7token なしlogin

2 モジュール構造

フロントエンド・Service Worker・Vercel API の3層構成です。

2.1 フロントエンド(TypeScript / React)

画面を構成するコンポーネント・フックの依存関係を示します。 また、図には現れない共通ユーティリティファイルも以下の表に一覧します。

表 2.1-1 共通ユーティリティ(viewer/ 配下)

Noファイル名役割
1types.tsフロントエンド全体で共有する型定義(DraftRecord 等)
2utils.ts依存を持たない汎用的なユーティリティ関数群
3editor-helpers.tsテキストエリア操作やタグ管理(localStorageとの連携)を担うヘルパー

図 3-2 フロントエンドモジュール構成

2.2 Service Worker(worker/index.js)

push受信・notificationclick等を処理するSWのイベントハンドラ5点を一覧します。

表 2.2-1 SW イベントハンドラ一覧

Noイベント処理内容
1installskipWaiting() 呼び出し。新バージョンを即時有効化。
2activateclients.claim() でページの制御を取得。SW バージョンをログに記録。
3push① Push ペイロード(title / body_rich / id)を取得
fusen-meta からアクセストークンを取得
③ Drive から画像をダウンロード
fusen-drafts にノートを保存
⑤ Drive から画像ファイルを削除
notes_to_iphone.json から当該 ID を削除
pending_openfusen-meta に記録
⑧ 既存の同 ID 通知を閉じてから新規通知を表示
4notificationclick通知をタップ → locked 確認 → true なら再通知・アプリを前面に出す。
⚠️ iOS では発火しない(既知の制約)。タップ後の再通知は page.tsxpending_open フローが代替。
5messageアプリからの通信を受信。CLOSE_NOTIFICATION で通知を閉じる、GET_VERSION で SW のバージョンを返す等の処理。

2.3 サーバーサイド(Vercel API Routes)

Google OAuth2 の client_secret を保護するVercel APIエンドポイント2点を説明します。

表 2.3-1 Vercel API Routes 一覧

Noファイル役割
1app/api/auth/token/route.tsOAuth 認証コード → アクセストークン+リフレッシュトークン交換。初回ログイン時のみ呼ばれる。
2app/api/auth/refresh/route.tsリフレッシュトークン → 新しいアクセストークン取得。Drive API 呼び出し時にトークン期限切れを検出したら自動呼び出し。

環境変数(Vercel)

Vercel の Environment Variables に設定する変数の一覧です。.env ファイルやコードにハードコードしないこと。

表 2.3-2 Vercel 環境変数一覧

変数名公開範囲取得元用途
GOOGLE_CLIENT_SECRET_PWAサーバー専用Google Cloud Console → 認証情報 → OAuth 2.0 クライアント(PWA用) → クライアント シークレットトークン交換・リフレッシュ時に Google へ提示する秘密鍵。ブラウザに渡してはいけない。
NEXT_PUBLIC_GDRIVE_CLIENT_ID公開可Google Cloud Console → 認証情報 → OAuth 2.0 クライアント(PWA用) → クライアント IDiPhone PWA の OAuth フロー開始時に使う。公開しても問題ない。
NEXT_PUBLIC_VAPID_PUBLIC_KEY公開可Web Push 鍵ペア生成時の公開鍵(web-push generate-vapid-keys 等で生成)iPhone の Web Push 購読登録(pushManager.subscribe)時に使う。
DISCORD_WEBHOOK_URLサーバー専用Discord サーバー → チャンネル設定 → 連携サービス → Webhook → URL をコピーPC 設定画面のフィードバック送信ボタンから Discord へ通知を転送する。

3 データ構造

IndexedDB・localStorage・Google Drive の3か所にデータを分散して保存します。

3.1 IndexedDB

PWA端末内に保存されるfusen-drafts・fusen-meta・fusen-logsの3ストアを定義します。

3.1.1 🗄 fusen-drafts(ノートデータ)

表 3-4 fusen-drafts スキーマ

Noフィールド意味
1idstring主キー(UUID)
2titlestringノートタイトル
3bodystring本文(Markdown)
4imagesObject[]添付画像({ fileName: string, blob: Blob }[]
5tagsstring[]付与されたタグの配列
6lockedbooleanロック画面に表示が ON なら true
7created_atstring作成日時(JST ISO 8601)
8sent_atstring送信日時(未送信時は undefined)
9received_pcbooleanPC 側が受信済みかどうか

3.1.2 🗄 fusen-meta(メタ情報)

表 3-5 fusen-meta スキーマ

Noキー意味
1access_tokenGoogle Drive アクセストークン(SW が参照)
2pending_open次回起動時に開くノートの情報。{ id: string, t: number }

3.1.3 🗄 fusen-logs(デバッグログ)

表 3-6 fusen-logs スキーマ

Noフィールド意味
1tタイムスタンプ(JST)
2msgログメッセージ

3.2 localStorage

セッション管理に使うlocalStorageのキーと値の一覧です。

3.2.1 🔑 認証・設定フラグ

表 3-7 localStorage キー一覧

Noキー意味
1viewer_access_tokenGoogle API アクセストークン
2viewer_refresh_tokenリフレッシュトークン(Vercel API 経由で更新)
3viewer_expires_atアクセストークンの有効期限(ms)
4viewer_push_done"true" なら通知設定済み → list へ直行
5pkce_verifierOAuth PKCE の code_verifier(認証中のみ存在)
6pending_notePKCE 認証後に自動で開くノート ID
7viewer_device_idWeb Push 用のクライアント識別子
8fusen_known_tags過去に入力したタグの履歴(サジェスト用)

3.3 Google Drive ファイル

PCとiPhone間の中継に使うDriveファイルの種類と書き込み/削除責務を説明します。

表 3.3-1 Google Drive ファイル一覧

Noファイル名書き込み読み取り・削除意味
1notes_to_iphone.jsonPC(gdrive.rs)iPhone SW
(push受信時)
PC → iPhone 未処理キュー。SW が受信して IndexedDB に保存後、当該 ID を除いて書き戻す(または全削除)。
2notes_from_iphone.jsoniPhone
(useBackgroundSend)
PC(gdrive.rs
30秒ポーリング)
iPhone → PC 未処理キュー。PC 受信後は処理済みアイテムを除いた残りのみ書き戻す。
3fusen_img_*.jpgiPhone
または PC
受信側が処理後に削除添付画像ファイル。
規則: fusen_img_YY..._N.jpg
4push_devices.jsoniPhone(lib/push.ts)
が upsert
PC(webpush.rs)が
全端末へ Push 送信
Web Push 宛先一覧。端末ごとの device_id / endpoint 等を保持。複数端末対応。

4 データフロー

起動・PC→iPhone受信・iPhone→PC送信・通知ON/OFFの4つのシーケンス図を示します。

4.1 アプリ起動(初回:Safari でインストール → Google ログイン → 通知許可)

初回起動(Safari→インストール→ログイン→通知許可)の全ステップを示します。

図 3-3 アプリ起動シーケンス(初回:インストール → 認証 → 通知許可)

4.2 PC → iPhone 受信(ユーザー体験 + 内部処理)

PCで「iPhoneに送る」を押した瞬間から、iPhoneのロック画面に通知が出て、ユーザーがタップして内容を確認するまでの全体フロー。

図 3-4 PC → iPhone 受信シーケンス

4.3 iPhone → PC 送信(ユーザー体験 + 内部処理)

iPhoneのwrite画面で「PCに送る」を押した瞬間から、PCに新しい付箋が開くまでの全体フロー。

図 3-5 iPhone → PC 送信シーケンス

4.4 ロック画面に表示 ON/OFF と再通知サイクル(REQ_IP_05

「消す意思がないかぎりロック画面から消えない」体験を実現する ON/OFF 操作と、タップ後の再通知サイクル。 ユーザー体験の手順は ①〜⑥ で示す(REQ_IP_05 の①〜④に対応)。

図 3-6 ロック画面常駐サイクル(ON/OFF と再通知の実装フロー)


5 UI インタラクション

5.1 画面モード定義

リスト・ライトの 2 モードとその遷移条件を定義します。

表 5.1-1 画面モード定義

Noモード状態遷移トリガー
1リストモードDrive から同期した付箋を一覧表示している状態アプリ起動後の初期画面
2ライトモード1 枚の付箋を全画面で編集している状態(800ms 自動保存)付箋タップ or +ボタン

図 3-7 画面モード遷移図

5.2 各モードの操作一覧

表 3-10 リストモードの操作

No操作対象結果
1タップ付箋アイテムライトモードへ遷移
2🔔 タップロック画面に表示ボタンlocked フラグの ON/OFF 切り替え
3🗑️ タップ削除ボタン確認ダイアログ → Drive から削除
4+ タップ新規ボタン空のライトモードへ遷移

表 3-11 ライトモードの操作

No操作対象結果
1文字入力エディタ800ms debounce で自動保存
2画像貼り付けエディタIndexedDB に保存・プレビュー表示
3← タップ戻るボタンリストモードへ遷移(保存確定)

5.3 インタラクション・マトリックス

表 5.3-1 インタラクション・マトリックス

No操作リストモードライトモード
1タップライトモードへ-
2🔔locked ON/OFF-
3🗑️削除確認ダイアログ-
4文字入力-自動保存
5← 戻る-リストモードへ

6 機能一覧

画面ごとの機能と、各機能の設計意図を記します。

6.1 メモ一覧画面(list)

表 6.1-1 メモ一覧画面の機能

機能設計意図・工夫
Drive → IndexedDB 同期一覧を開くたびに notes_to_iphone.json を Drive から取得し、ローカルにない新着ノートを IndexedDB に取り込む。取り込み後は Drive ファイルを削除(Drive = 未処理キュー)。Drive 失敗時は IndexedDB だけで一覧表示を続ける(フォールセーフ)
画像サムネイル添付画像がある場合、IndexedDB の Blob から URL.createObjectURL() で URL を生成してサムネイルを表示。アンマウント時に URL.revokeObjectURL() で解放する
ステータスバッジdraft(下書き)/ sent(PC送信済み)/ PC受信 の3状態を sent_at フィールドの有無と received_pc フラグで判定して色分け表示する
相対時間表示created_at から「3分前」「1時間前」「昨日」の形式に変換(formatRelativeTime())。 数字と絶対時刻を並べるより一目で新鮮度がわかる
🔔/🔕 ロック画面常駐後述(6.2)
🗑️ 削除IndexedDB から削除後、Drive 上の同 ID ファイルも削除する
+ 新規作成新しい下書き ID を crypto.randomUUID() で生成し write 画面へ遷移
🔔 デバイス再登録(フッター)silentReRegisterIfNeeded() を呼び出し、push_devices.json に自デバイスが存在しない場合のみ静かに再登録する。既存デバイスがいれば何もしない

6.2 ロック画面常駐(🔔)

このアプリの核心機能。「消す意思がないかぎり、ロック画面から消えない」体験を実現する。

表 6.2-1 ロック画面常駐の仕組みと設計意図

観点設計意図・工夫
通知表示の方式iOS ではメインスレッドの new Notification() が動かない。navigator.serviceWorker.readyreg.showNotification() を使う。これが iOS で通知を出せる唯一の方法
重複防止reg.getNotifications() で既存通知を取得し、同じ data.id を持つものをすべて n.close() してから新規通知を表示する
タイトル・本文の生成# タイトル 行があれば it を通知タイトルに。なければ本文冒頭20文字をタイトルに使う。本文から画像タグ ![](...) を正規表現で除去してから40〜60文字を表示する
楽観的更新ボタンを押した瞬間に UI の 🔔/🔕 を切り替える。SW 操作や IndexedDB 書き込みに失敗した場合のみロールバックする。ユーザーに「レスポンスが遅い」と感じさせない
通知権限の動的確認Notification.permission === 'default' なら許可ダイアログを表示。denied なら UI を元に戻してエラーを表示する
通知を消す権限ロック解除時に reg.active?.postMessage({ type: 'CLOSE_NOTIFICATION', tag }) で SW に通知クローズを依頼する。メインスレッドは通知を直接閉じられない
locked フラグの永続化locked: true/false を IndexedDB に書き込む。次回 Push 受信時に SW がこの値を参照して再通知を行うかどうかを決める
効果音ON 時は bell_on.wav、OFF 時は bell_off.wavAudio API で再生。操作の結果をユーザーが音で確認できる

6.3 メモ編集画面(write)

表 6.3-1 メモ編集画面の機能

機能設計意図・工夫
contenteditable エディタcontentEditable="true"div で実装。React の value/onChange モデルを使わず DOM を直接操作。serializeEditor() が HTML → Markdown 形式に変換する
3秒自動保存入力のたびにタイマーをリセット。3秒間入力がなければ IndexedDB に保存(useAutoSave)。Drive は使わない。ネットワーク不要・オフライン動作
バックグラウンド遷移時の強制保存visibilitychange で非表示になった瞬間にも保存(useVisibilitySave)。アプリを閉じてもデータが消えない
📷 画像添付ファイル選択 → CropModal でトリミング → IndexedDB に Blob 保存 → buildImageFileName() でファイル名生成 → カーソル位置に ![]() を挿入。PCへ送るときに Drive に実際にアップロードされる
🔷 MermaidMermaidModal でフローチャートのコードを書いて確定するとエディタに挿入される
☑ チェックボックスカーソルのある行を data-checkbox-line 属性を持つ <span> に変換(再押しで解除)。行頭が IMG ノードの場合は次の兄弟ノードに処理を移す特殊ケース対応あり
🏷️ タグタグバーを展開。Enter または Space で確定。過去に入力したタグを localStorage(fusen_known_tags) から自動サジェスト。送信時に mergeKnownTags() で履歴に追加
← 一覧に戻る内容がある場合は IndexedDB に下書き保存してから list 画面へ遷移。保存はサイレントに行われ、確認ダイアログは不要
「iPhoneに置いておく」Drive を一切使わず IndexedDB のみに保存。ネットワーク不要。削除するまで端末に残り続ける
「PCへ送る」後述(6.4)

6.4 「PCへ送る」

表 6.4-1 PCへ送る の処理ステップ

ステップ設計意図・工夫
① トークン有効期限確認viewer_expires_atDate.now() を比較。期限5分前を切っていたら送信前に Vercel /api/auth/refresh を呼んでトークンを更新する。送信中に突然期限切れにならないための先読み更新
② セッション切れ処理リフレッシュが失敗した場合は localStorage のトークンを削除し、login 画面へ遷移。エラーメッセージを5秒表示して消す
③ 画像アップロード添付画像を Promise.all() で並列アップロード。直列より速い
④ キューへの追記notes_from_iphone.json を Drive から読み取り、既存アイテムの末尾に新しいアイテムを追加して上書き(read-modify-write)。旧スキーマのファイルが残っていても自動変換して引き継ぐ後方互換処理あり
⑤ IndexedDB に sent を記録送信後、同 ID のレコードに sent_at をセットして IndexedDB を更新。一覧画面に「送信済み」バッジが表示される
⑥ 成功フィードバック3秒間 backgroundSendSuccess = true にして UI に成功インジケータを表示。その後自動で消える

6.5 通知許可・デバイス登録(push 画面)

表 6.5-1 プッシュ通知登録の処理ステップ

ステップ設計意図・工夫
① 通知権限取得Notification.requestPermission() で OS レベルの許可ダイアログを表示。拒否された場合はエラーを表示して処理を停止
② 購読の再生成既存の Push 購読があれば一度 unsubscribe() してから再登録する。クリーンな状態を保つためのリセット処理
③ VAPID 鍵での購読process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY(Vercel 環境変数)を使って pushManager.subscribe() を実行。この鍵は PC 側の WebPush 送信時の署名鍵と対になる
④ デバイス ID の永続化crypto.randomUUID() で端末固有の device_id を生成。localStorage に保存し以降の再登録でも同一 ID を使う
⑤ push_devices.json への upsertDrive から push_devices.json を取得し、同 device_id のエントリを更新(または新規追加)して上書き保存。旧スキーマ(endpoint 直下方式)は自動的に新スキーマに移行する

7 エラーハンドリング・リカバリ方針

7.1 通信・認証エラー

7.1.1 Drive API エラーとトークン自動更新

Drive API 呼び出し(ダウンロードやアップロード)が失敗した場合、drive.ts 内の downloadWithAutoRefresh 等によりトークンの自動リフレッシュが試行される(実施済み)。 リフレッシュにも失敗した場合は例外がスローされ、呼び出し元の React コンポーネント側でキャッチされる。 UI上のエラーフィードバック(トースト表示等)は ⚠️ 未実施

7.1.2 認証切れ時のフォールバック

トークンリフレッシュ(Vercel API /api/auth/refresh)が 4xx 等で失敗し、リフレッシュトークン自体が失効していると判定された場合、localStorage からトークン情報を破棄し null を返す。 これによりアプリは未認証状態とみなされ、自動的にログイン画面(login ステップ)へフォールバックする(実施済み)。

7.1.3 PCアプリ側での Drive API 失敗(gdrive.rs)

PC アプリ(Rust)が Drive API 呼び出しに失敗した場合、Err(String) を返して Tauri コマンド経由でPCフロントエンドにエラーを通知する。 PC側のアクセストークンは期限到来の 60 秒前に自動リフレッシュされ、失敗時は「Googleの認証が切れました」と返す。 PCフロントエンドでのエラーダイアログ表示(トースト等)は ⚠️ 未実施

7.1.4 PCからの Web Push 送信失敗

PC アプリから APNs / FCM へのプッシュ送信が失敗した場合(201 以外の HTTP ステータス)、エラーコードを含む Err を返す。 **PC側での送信失敗時の自動リトライ機構は ⚠️ 未実施。**送信失敗時は iPhone に通知が届かないまま終了する。

7.2 バックグラウンド処理・リカバリ

7.2.1 Push 受信時のフォールバック(画像DL失敗時)

Service Worker (worker/index.js) 内での Push 受信処理では、画像(fusen_img_*)の Drive ダウンロードがネットワークエラー等で失敗した場合でも処理を中断しないフェイルセーフ機構がある。 画像取得に失敗した場合でも、テキスト本文のみを IndexedDB(fusen-drafts)に保存し、ユーザーへの OS 通知を確実に表示する(実施済み)。

7.2.2 デバッグログの運用方針(fusen-logs)

UIを持たない Service Worker 内で発生した処理結果やエラー(トークン取得失敗、画像保存失敗など)は、IndexedDB の fusen-logs ストアに対して fire-and-forget で記録される(実施済み)。 後から Chrome DevTools 等で内部状態や Push 受信時のエラー原因を追跡できるようになっている。

7.2.3 iOS特有の制約とリカバリサイクル

iOS の PWA 環境では、バックグラウンドでの通知タップ時(notificationclick イベント)が正常に発火しない・あるいは Web API へのアクセスが制限されるケースがある。 この制限に対するリカバリとして、通知受信時に次回開くべきノート ID を IndexedDB に保存(pending_open)し、次にユーザーがアプリを開いた際(page.tsx マウント時)に自動的にそのノートを表示するサイクルを構築している(実施済み)。

7.2.4 Service Worker の更新

skipWaiting() + clients.claim() で新しい SW が即時有効化される。バグ修正版をリリースした際に古い SW が動き続けることはない。


8 改版履歴

表 8-1 改版履歴

Noバージョン日付変更内容
11.026-04-19新規作成。005_VIEWER_SCREENS.html / 007_VIEWER_CODE_STRUCTURE.html / 004_PWA_DATA_FLOW.html の内容を統合・整理
21.126-04-204.4 にロック画面常駐体験(REQ_IP_05)の再通知サイクル(①②③④)を追加。4.2 に REQ_IP_05 への参照を追加
31.226-04-204.4 の再通知フローを実態に合わせて修正。iOS では notificationclick が発火しないため pending_open + page.tsx が再通知を担う仕組みを図入りで明記。2.2 の notificationclick 説明に iOS 制約を追記
41.326-04-24モジュール構造図を graph LR(横向き)に変更。スクロールなしで全体が見えるよう改善。
51.426-04-27セクション6「機能一覧」を新規追加。メモ一覧・ロック画面常駐・メモ編集・PCへ送る・プッシュ登録の設計意図を記載。旧6→7、旧7→8に繰り下げ。