概要
| 項目 | 内容 |
|---|
| 案件ID | MAS-238 |
| カテゴリ | プラットフォーム |
| Phase | P3(GCP移行・SaaS化) |
| 優先度 | ★★ |
| ステータス | 未着手 |
| 対象ファイル(新規作成) | 300_ui/302_webapp_api.js(GAS Web App JSON API エントリーポイント) |
| 変更ファイル | appsscript.json(Web App 設定追加。oauthScopes を変更する場合は完全列挙が必須) |
| 前提案件 | MAS-236(認証・ユーザー管理基盤)・MAS-239(API層 REST/GraphQL)・MAS-187(RBAC — 本 TODO_future.md に MAS-238 時点では未起票のため、MAS-187 案件が追加された際は本仕様書を更新すること) |
目的
スプレッドシート UI から Web アプリ(Next.js on Cloud Run)へ移行し、スプレッドシートの制約(同時編集・表示速度・UI自由度の限界)を解消する。GAS を JSON API サーバーとして活用することで既存の会計ロジック(RPA・仕訳エンジン・データマート生成)を再利用しながら、経営者のスマホ閲覧ユースケースを含むレスポンシブな Web UI を提供する。
現在のコード(移行前の状態)
現在の UI 層は 300_ui/301_ui_assist.js に実装されている。このファイルは以下の公開関数・処理を担う:
handleUxAssist(e) — onEdit トリガーによるリアルタイム入力補完(シート名・ヘッダー名ベースの動的補完)
applyPartnerPaymentTerms_(sheet, row, headers, partnerName) — 取引先マスタの決済条件を空欄セルに転記(MAS-120 実装済み)
メニュー定義は 000_infra/002_constants.js の Constants.MENU_DEFINITION で一元管理されており、onOpen() からカテゴリ 🚀 BizLP(openOperationsSidebar / installAutoOpenSidebarTrigger / uninstallAutoOpenSidebarTrigger)と 💾 バックアップ(runManualBackup / installBackupTriggers / verifyLatestBackup / uninstallBackupTriggers / updateMenuCatalog_)が生成される。
現時点のコードには doGet(e) / doPost(e) エントリーポイントが存在しない。GAS Web App として機能させるには、これらを新規ファイルに実装する必要がある。
移行方針(Step 分割)
アーキテクチャ決定事項
- JSON API 構成: GAS Web App(
doGet(e) / doPost(e))を JSON API サーバーとして利用する。新規ファイル 300_ui/302_webapp_api.js を作成する(CLAUDE.md の GAS ファイル番号体系に基づき 300_ui/ レイヤーの 302 番を採番)
- フロントエンド: Next.js on Cloud Run でホスティング。画面構成はダッシュボード(KPI一覧)・財務3表ビューア・PJ損益・RPA実行・設定画面を想定
- 認証方式: MAS-236(認証基盤)の完了を前提とする。JWT 検証ロジックは MAS-236 仕様確定後に詳細設計(現時点では「要調査: MAS-236 完了待ち」とし、全 API 冒頭に認証スタブを挿入しておく)
- データ取得: 必ず Repository 層(
OrderRepository.findAll() / InvoiceRepository.findAll() / BankTxRepository.findAll() / JournalRepository.findAll() 等)を経由する。API ハンドラでの SpreadsheetApp 直接操作は禁止
- KPI・財務3表データソース:
600_report/ の公開関数を呼び出す。KPI は buildKpiDashboard()(609_datamart_kpi.js)、P/L マート更新は buildBudgetTrendDataMart()(602_datamart_main.js)を使用する
- POST 系排他制御:
LockService.getScriptLock() による排他ロックを必須とする。lock.tryLock(10000) で取得し、失敗時は { status: 'processing', message: '処理中です。しばらくしてからリトライしてください' } を返す
- レスポンス形式:
Contracts.toDtoList() で変換した DTO を基本とし、ContentService.createTextOutput(JSON.stringify(...)).setMimeType(ContentService.MimeType.JSON) で返す
移行ステップ
| Step | 内容 | 担当 | 状況 |
|---|
| Step 1 | GAS Web App API エンドポイント設計・300_ui/302_webapp_api.js 新規作成 | GASデベロッパー | 未着手 |
| Step 2 | Next.js フロントエンド画面実装・Cloud Run 構成 | フロントエンドエンジニア | MAS-236 完了待ち |
| Step 3 | MAS-236 認証統合・本番デプロイ設定 | GCPアーキテクト | MAS-236 完了待ち |
影響範囲
| ファイル | 変更種別 | 内容 |
|---|
300_ui/302_webapp_api.js | 新規作成 | doGet(e) / doPost(e) ルーター。全エンドポイントに認証スタブ挿入 |
appsscript.json | 変更(条件付き) | Web App 設定追加。現在は "webapp": { "access": "MYSELF", "executeAs": "USER_DEPLOYING" } が設定済み。oauthScopes を変更する場合は GAS エディタ「プロジェクト設定 → OAuth スコープ」で使用中スコープを全列挙してから追記 |
300_ui/301_ui_assist.js | 変更なし(並行移行期間中は旧 UI を維持) | — |
注意事項
appsscript.json の oauthScopes を部分宣言すると既存の SpreadsheetApp / DriveApp / MailApp 等が全て「Specified permissions are not sufficient」で動作不能になる(失敗パターン #26)。Web App 設定追加時は GAS エディタ「プロジェクト設定 → OAuth スコープ」で使用中の全スコープを確認し完全列挙すること。なお現在の appsscript.json には oauthScopes フィールドが存在しない(enabledAdvancedServices で Sheets v4 / AdminReports を管理)ため、不要であれば oauthScopes を追加しないことが最も安全
Session.getActiveUser().getEmail() は GAS Web App の「アクセスできるユーザー」設定に依存する。「全員(匿名を含む)」設定では常に空文字列を返すため、リクエスト元特定は JWT に一本化すること
- GAS Web App の
doGet(e) / doPost(e) は ContentService.createTextOutput() でテキスト/JSON を返す。バイナリ・Server-Sent Events・WebSocket は非対応
LockService.getScriptLock() はスクリプト単位のグローバルロック。tryLock(10000)(10 秒待機)が推奨値。取得失敗時は HTTP 200 + エラーステータス JSON を返す(HTTP 429 は GAS の ContentService では返せない)
- GAS の 1 リクエスト最大実行時間は 6 分。データマート全更新等の重い処理を同期 API で待機させない(エッジケースセクション参照)
エッジケース
| 条件 | 表示値/挙動 | 理由・対策 |
|---|
| GAS 実行時間 6 分超(データマート全更新等) | { status: 'queued', message: '非同期実行を開始しました' } を即時返却 | 同期 API で待たず、ジョブ投入 API → ポーリング方式に設計。詳細は「人間が検討すべき事項」参照 |
| 大量レコード取得(仕訳帳全件等) | limit(デフォルト 100)/ offset(デフォルト 0)パラメータで分割返却 | GAS レスポンスサイズ上限・実行時間の制約内に収める |
| POST 系 API のロック取得失敗 | { status: 'processing', message: '処理中です。しばらくしてからリトライしてください' } | LockService.tryLock(10000) が false の場合。フロントでリトライ UI を表示 |
| データなし(売上ゼロ月・空の仕訳帳等) | { data: [], total: 0 } | null / undefined を返さず空配列で統一 |
| MAS-236 未完了(JWT 検証未実装) | 全 API が認証スタブを通過(暫定) | 全 API 冒頭に // TODO: G-01 完了後に JWT 検証を実装 を挿入しておく |
有効フラグ = FALSE のレコード | API レスポンスに含めない | CLAUDE.md の規約「有効フラグ=FALSE の行は全処理でスキップ」に準拠。readSheetAsDtos_() で DTO 変換後に 有効フラグ が false の行をフィルタする |
RPAService.generateAll() 等の複合 POST 処理 | 各 RPA 関数が SpreadsheetApp.getUi().alert を呼び出すため Web App から実行不可 | GAS Web App では SpreadsheetApp.getUi() がエラーになる。Step 1 では silent=true パラメータを使用するか、UI 呼び出しを除いた専用ラッパー関数を実装する必要がある(RPAService.generateSaas(null, true) 等 silent モードで呼び出し) |
実データ検証
実装前に MCP 等で以下を確認すること:
- 各シート(
31_wrk_order / 32_wrk_invoice / 33_wrk_bank / 42_trn_journal)のヘッダー行が 000_infra/003_contracts.js の DTO @typedef プロパティ名と一致しているか(列ずれは DDL 変更等で発生しうる)
Utils.getSheetByKey が参照する 01_sys_config(Constants.CONFIG_SHEET)シートに全シートキー(WRK_ORDR / WRK_INVC / WRK_BANK / TRN_JOUR 等)が登録されているか
appsscript.json の現在の webapp 設定(access / executeAs)と enabledAdvancedServices の内容を Read して確認(失敗パターン #26 の回避のため)
buildKpiDashboard() および buildBudgetTrendDataMart() の実行時間を dev 環境で計測し、6 分制限に近い場合は非同期化の優先順位を上げる
関連ドキュメント
| ドキュメント | 関連箇所 |
|---|
CLAUDE.md | GAS 環境分離(Env モジュール)・デプロイフロー・ファイル番号体系 |
000_infra/003_contracts.js | DTO 型定義(API レスポンスのデータモデル) |
200_data/202_repository.js | Repository 層インターフェース(API ハンドラのデータ取得基盤) |
docs/_internal/failure_patterns.md | #26(oauthScopes 排他制御)・#27(外部 API 仕様変動) |
docs/_internal/TODO_future.md | MAS-236(認証基盤)・MAS-239(API層)案件定義 |
appsscript.json | 現在の Web App 設定・enabledAdvancedServices 設定 |
人間が検討すべき事項
TODO_future.md の MAS-238 記載事項(スプレッドシート UI からの移行・レスポンシブ対応・技術選定)に加え、以下を意思決定する必要がある:
- ジョブキュー設計: 6 分超の処理(
buildBudgetTrendDataMart() 等のデータマート全更新)の非同期化手段。GAS Time-based Trigger を使った疑似ジョブキュー(ジョブ投入 → 別トリガーで実行 → ポーリング確認)vs Cloud Tasks 等の外部サービス利用。GAS 内で完結させると状態管理がシート依存になる
- 認証方式の暫定戦略: MAS-236(Firebase Auth)完了前の段階での認証スタブの粒度。「全員許可(oauthScopes 設定で access: ANYONE_WITH_GOOGLE_ACCOUNT)」にすると Google アカウント所持者なら誰でもアクセス可能になる。会社ドメイン制限 or IP 制限の暫定措置を検討する
- RBAC 設計: MAS-187(RBAC)は TODO_future.md に現時点で未起票のため、MAS-238 Step 1 段階での権限チェックスタブの粒度(メールアドレスホワイトリスト vs ロールテーブル参照)を決定する
- GAS Web App デプロイ URL 管理:
clasp deploy 実行のたびにデプロイ ID が変わり Web App URL が変わる問題。Cloud Run 側の API エンドポイント設定(環境変数)の更新自動化戦略(または GAS の「最新バージョン」を指す固定 URL の活用)
- モバイル対応: 経営者のスマホ閲覧ユースケースにおける KPI ダッシュボードの優先表示項目の選定(全 KPI 表示は困難なため絞り込みが必要)
RPAService の Web App 対応化: 既存の RPA 関数(RPAService.generateAll() 等)は SpreadsheetApp.getUi().alert に依存しており、GAS Web App から直接呼び出せない。silent=true の活用範囲と、UI を前提としない専用ラッパー関数の設計方針を確定する
- スプレッドシート UI との並行稼働期間: Web UI 完成後もスプレッドシート UI(
300_ui/301_ui_assist.js・サイドバー)を維持する期間の定義と、並行稼働中のデータ整合性保証方針
実装プロンプト(Claude Code 用)
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-238「Web UI / フロントエンド」の Step 1(GAS Web App API エンドポイント設計)を実装してください。
## 実行前タスク
- `appsscript.json`: 現在の oauthScopes 設定を Read して確認(失敗パターン #26 回避のため必須)
- `000_infra/001_env.js`: Env モジュールの公開メソッド(`Env.isDev()` / `Env.spreadsheetId()` 等)を Read
- `000_infra/003_contracts.js`: DTO 型(OrderDTO / InvoiceDTO / BankTxDTO / JournalEntryDTO)と `Contracts.toDtoList()` の戻り値型を Read
- `200_data/202_repository.js`: 各 Repository の `findAll()` 戻り値型(`{ headers, dtos }` の構造)を Read
- `400_domain/407_rpa_orchestrator.js`: `RPAService` の公開 API 関数名と `silent` パラメータの使用方法を Read して特定
- `600_report/602_datamart_main.js` および `600_report/609_datamart_kpi.js`: データマート生成の公開関数名(`buildBudgetTrendDataMart` / `buildKpiDashboard`)を Read で確定
- `300_ui/301_ui_assist.js`: 既存 UI ヘルパーの公開関数名を Read(変更禁止対象の確認)
- `CLAUDE.md`: ファイル番号体系(「GAS ファイル番号体系」セクション)で `300_ui/` の採番ルールを確認
## 修正対象ファイル
- **新規作成**: `300_ui/302_webapp_api.js`(`doGet(e)` / `doPost(e)` エントリーポイント)
- **変更**: `appsscript.json`(Web App 設定確認。`oauthScopes` を変更する場合は必ず完全列挙。現在 `oauthScopes` は未設定のため、不要なら追加しないことが最も安全)
## 実装内容
1. `300_ui/302_webapp_api.js` に `doGet(e)` / `doPost(e)` ルーターを実装。クエリパラメータ `action` で処理を振り分ける
2. 全エンドポイントの冒頭に認証スタブを挿入:
```javascript
// TODO: G-01 完了後にJWT検証を実装
// var email = verifyJwt_(e.parameter.token); // G-01実装後に有効化
```
3. POST 系エンドポイント(RPA 実行等)に排他ロックを実装:
```javascript
var lock = LockService.getScriptLock();
if (!lock.tryLock(10000)) {
return ContentService.createTextOutput(
JSON.stringify({ status: 'processing', message: '処理中です。しばらくしてからリトライしてください' })
).setMimeType(ContentService.MimeType.JSON);
}
```
4. データ取得系 GET エンドポイントは `Repository.findAll()` を経由し、`ContentService.createTextOutput(JSON.stringify({ data: dtos, total: dtos.length })).setMimeType(ContentService.MimeType.JSON)` で返す
5. 全 GET エンドポイントにページネーション実装(`parseInt(e.parameter.limit) || 100` / `parseInt(e.parameter.offset) || 0`)
6. `Utils.auditLog('RUN', '', '', '', 'doPost', null, action)` で POST 系操作を監査ログに記録
7. RPA 実行エンドポイントでは `RPAService.generateSaas(null, true)` のように `silent=true` を渡す(Web App では `SpreadsheetApp.getUi().alert` が呼べないため)
## 制約
- API ハンドラ内で `SpreadsheetApp` を直接操作しない(必ず Repository 層を経由)
- `300_ui/301_ui_assist.js` は変更しない(並行移行期間中は旧 UI を維持)
- `appsscript.json` の `oauthScopes` を追記する場合、GAS エディタ「プロジェクト設定 → OAuth スコープ」で使用中の全スコープを確認し、**完全列挙**してから新規スコープを追加する(部分宣言禁止)
- 仕様書に記載のない関数名・シート名を推測で使用しない。不明点は `Read` で確認してから実装する
## エッジケース
- GAS 実行時間 6 分超の可能性がある処理(`buildBudgetTrendDataMart()` 等): 同期待ちせず `{ status: 'queued', message: '非同期実行を開始しました' }` を即時返却(ジョブ管理の詳細設計は Step 2 以降)
- ロック取得失敗: `{ status: 'processing', message: '処理中です。しばらくしてからリトライしてください' }`
- データなし: `{ data: [], total: 0 }` で統一(null / undefined を返さない)
- `有効フラグ = FALSE` のレコード: `readSheetAsDtos_()` で全行取得後、`dto['有効フラグ'] !== false` でフィルタしてから返す(`202_repository.js` の実装を Read して確認すること)
## 動作確認
1. `npm run push:dev` で開発 GAS にデプロイ
2. GAS エディタで「デプロイ → 新しいデプロイ → ウェブアプリ」を手動実行し、Web App URL を取得
3. curl で `GET {URL}?action=invoices&limit=10` を実行。HTTP 200 + `{ data: [...], total: N }` を確認
4. POST 系エンドポイントを並列 2 リクエストで送信し、片方が `{ status: 'processing' }` を返すことを確認(ロック動作確認)
5. `appsscript.json` の oauthScopes を変更した場合、既存機能(操作パネルサイドバー・手動バックアップ・RPA 自動起票)の動作を一通り確認してから `git push` する
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read) | あり | ファイル番号・関数名・oauthScopes の確定 |
| 実装 | なし | Phase 1 確定内容の書き下しのみ |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|
| Step 1: GAS Web App API エンドポイント設計(新規ファイル作成) | Claude Sonnet 4.6 | 複数ファイル横断の挿入位置特定・既存 Repository パターンの適用が必要 |
| Step 2: Next.js フロントエンド画面実装・Cloud Run 構成 | Claude Opus 4.6 | 複数画面の設計判断・GAS API との統合設計が必要 |
| Step 3: MAS-236 認証統合・本番デプロイ設定 | Claude Opus 4.6 | 外部サービス(Firebase Auth)との統合設計・インフラ判断が必要 |
変更履歴
仕様書作成プロンプト
展開して表示
<instruction>
【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】
1. **拡張思考の使い分け**: Phase 1(設計)ではフル活用し、ファイル名形式・エッジケース一覧・固有名詞(関数名/シート名/列名/行番号)を完全確定させる。Phase 2(清書)では各 Step 内の拡張思考を最小限に抑え、Phase 1 確定済み内容の書き下しに徹する。出力途中で再考しない。
2. **テキスト報告の禁止**: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
3. **4-5 分割の Write/Edit 実行**:
- 2-1 骨格 Write(~20行)
- 2-2 概要〜注意事項 Edit/Bash(~300行)
- 2-3a エッジケース〜人間が検討すべき事項 Edit/Bash(~200行)
- 2-3b 実装プロンプト・推奨実行モデル・変更履歴 Edit/Bash(~250行)
- 2-4 `<details>` にプロンプト全文記録 Edit/Bash(最重量・必ず独立 Step)
4. **各 Step で何を書くかを具体指示**: 設計判断を Phase 2 実行時に持ち込まない。各 Step の内容は下記に完全列挙済み。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 G-03「Web UI / フロントエンド」の開発仕様書を作成してください。
開発仕様書を新規作成した場合は、`docs/_config.json` の `nav` 配列の適切なセクションにも必ず追記してください。
---
## Phase 1: 実行前タスク(テキスト報告禁止。即座にツール実行)
### 1-A: 案件定義・プロジェクト規約の読み込み
1. `docs/_internal/TODO_future.md` を検索し、**G-03** の案件名・カテゴリ・Phase・優先度・概要・人間が検討すべき事項を取得する。あわせて関連案件 **G-01(認証基盤)**・**G-04(API層)**・**N-11(RBAC)** の行が存在する場合は内容を把握する(存在しない場合はその旨を仕様書に記載する)。
2. `CLAUDE.md` を読み込み、ブランチ運用・GAS環境分離(`Env` モジュール経由の環境判定)・ファイル番号体系・コーディング規約(列参照ルール、有効フラグ処理等)を把握する。
### 1-B: 参考テンプレートの読み込み
- `docs/dev/dev_mas-001_variance_analysis.md` を読み込み、新機能(大規模・複数 Step 分割)仕様書のフォーマットを把握する。
### 1-C: 既存コードの調査(Grep は発見まで。「どう書くか」の判断は必ず Read で裏取りすること)
**推測で仕様書に固有名詞を書かない(失敗パターン #18-#20)。** 以下のファイルを順に Read し、各確認ポイントを把握してから Phase 2 に進むこと。
| ファイル | 確認ポイント |
|---------|------------|
| `000_infra/001_env.js` | `Env` モジュールの公開メソッド名(`Env.name()` / `Env.isDev()` / `Env.isProd()` / `Env.spreadsheetId()` 等)。Web App 化時の環境分岐に使う |
| `000_infra/002_constants.js` | `Constants.MENU_DEFINITION` の実際のカテゴリ名・ラベル名・`funcName`(動作確認手順で引用するため)。`Constants.SHEET_DEFAULTS` / `Constants.ID_PREFIX_MAP` の構造も確認 |
| `000_infra/003_contracts.js` | DTO の `@typedef` 一覧(`OrderDTO` / `InvoiceDTO` / `BankTxDTO` / `JournalEntryDTO` 等のプロパティ名)と `Contracts.toDto` / `Contracts.toDtoList` / `Contracts.toRow` の引数・戻り値型 |
| `000_infra/004_utils.js` | 実在する公開ヘルパー関数名(`Utils.getSheetByKey` / `Utils.getSheetNameByKey` / `Utils.parseDateToYm` / `Utils.auditLog` / `Utils.logInfo` / `Utils.logError` 等) |
| `200_data/202_repository.js` | 各 Repository(`OrderRepository` / `InvoiceRepository` / `BankTxRepository` / `JournalRepository` / `AccountRepository` / `PartnerRepository`)の `findAll()` / `save()` / `append()` の引数・戻り値型。内部ヘルパー(`readSheetAsDtos_` / `appendDtosToSheet_`)の役割も確認 |
| `300_ui/301_ui_assist.js` | 既存 UI ヘルパーの公開関数名と役割(Web UI 移行で廃止・維持・移植する処理の判断材料) |
| `400_domain/407_rpa_orchestrator.js` | `RPAService` の公開 API 関数名(RPA実行画面が呼び出すべきエントリーポイントを特定) |
| `400_domain/410_subledger_engine.js` | `SubledgerService` の公開 API 関数名 |
| `600_report/601_datamart_ingest.js` 〜 `608_datamart_render.js` | KPI・財務3表生成の公開関数名(ダッシュボード・財務3表ビューアが呼び出すデータソースを特定。Grep で関数名を一覧してから Read で引数・戻り値を確認) |
| `appsscript.json` | 現在の `oauthScopes` 設定の有無と内容(失敗パターン #26:部分宣言で既存機能が全滅するリスクを回避するために必須確認) |
| `docs/_internal/failure_patterns.md` | #21-#27(GAS 数式落とし穴・並列実装対称性漏れ・oauthScopes 排他制御・外部 API 仕様変動)の失敗パターンを学習 |
---
## Phase 2: 仕様書の分割作成
**出力先**: `docs/dev/dev_mas-238_web_ui.md`(ファイル名の ID 部分は大文字 `MAS-238`)
**【絶対に 1 回のツール呼び出しで全内容を出力しない。以下の Step に厳密に分割して実行すること。】**
---
### Step 2-1: 骨格の作成(Write / ~20行)
(骨格の見出し一覧は省略)
### Step 2-2: 概要〜注意事項の追記(Edit or Bash heredoc / ~300行)
Phase 1 で Read・確定した固有名詞のみを使用して以下を記述する(推測で関数名・シート名を書かない)
### Step 2-3a: エッジケース〜人間が検討すべき事項の追記(Edit or Bash / ~200行)
### Step 2-3b: 実装プロンプト・推奨実行モデル・変更履歴の追記(Edit or Bash / ~250行)
### Step 2-4: `<details>` にプロンプト全文を記録(Edit or Bash / 最重量・独立 Step)
---
## Phase 3: 保存・登録・コミット
### 3-A: `_config.json` への追記(必須)
`docs/_config.json` の §E.1(基盤・DevOps)セクションに以下を追記する
### 3-B: `changelog.md` への追記(必須)
`docs/_internal/changelog.md` の先頭行(ヘッダーの直後)に追記する
### 3-C: コミット&プッシュ
(コミットメッセージは省略)
</instruction>