Chrome拡張 事業者設定エクスポート/インポート機能
事業者設定を年度ごとに手作業で確認してスプレッドシートに転記していた作業を、Chrome拡張のボタン一発で全年度マトリクスとして出力できるようにした。さらにスプレッドシート上で編集した設定値をインポートで書き戻す双方向フローまで一日で組み上げた記録。
事業者設定タブの新設
Chrome拡張の設定画面に「事業者設定」タブを追加した。既存の「自動仕訳ルール」タブや「エクスポートログ」タブと同じミラーカラムUIパターンに載せる形で、エクスポートボタンとインポートUIを配置する。
タブ追加自体は popup.html にタブヘッダーとコンテンツ領域を足すだけだが、タブ切替ロジックが既存の手書きスイッチャーに依存していたため、新しいタブIDを配列に追加して回った。
エクスポート機能: 設定ページのスクレイピング
全年度マトリクスの設計
会計ソフトの事業者設定ページ(/offices/edit)には、事業者番号、事業者名、業種区分、会計期間など数十の設定項目がフォーム要素として並んでいる。これを年度ごとにスクレイピングして、横軸=年度・縦軸=設定項目のマトリクスをスプレッドシートに出力する。
年度切替はCTI(Client-side Table Index)の既存ロジックを流用し、各年度に移動してからDOMを読み取る。
事業者番号の取得
事業者番号はフォーム要素ではなくページ上部の表示テキストに埋まっていた。querySelector で取得するセレクタを追加し、マトリクスの先頭行に出力するようにした。
業種区分のラベル変換
業種区分は <select> 要素の value がIDで、画面に表示されるのは日本語のラベル。value をそのまま書き出すと「1001」のような数値になってスプレッドシート上で意味が分からない。
selectedOptions[0].textContent で表示中のラベルを取得する方式に切り替えた。
// ID値ではなく表示ラベルを取得
const industrySelect = doc.querySelector('select[name="industry_code"]');
const industryLabel = industrySelect?.selectedOptions[0]?.textContent?.trim() ?? '';
シート1削除タイミングの修正
Google Sheets APIで新規スプレッドシートを作成すると、デフォルトで「シート1」が生成される。当初はスプレッドシート作成直後にシート1を削除していたが、シートが1枚もない状態でデータ書き込みシートを追加しようとするとAPIがエラーを返した。
修正として、エクスポート用シートとインポート用シートの2枚を先に addSheet で追加してから、最後にシート1を deleteSheet する順序に変えた。
Codexレビュー3回
エクスポート機能の実装計画をCodexに3回投げた。1回目で「年度切替失敗時にマトリクスが歯抜けになるがエラーハンドリングがない」という指摘が返ってきて、年度切替失敗時はその年度列を "ERROR" で埋める処理を追加した。2回目で「スクレイピング対象のセレクタがハードコードされている」と指摘されたが、これは対象ページの構造に依存するため現状維持とした。3回目は軽微な命名指摘のみで、そのままマージした。
エクスポート時の2シート構成
エクスポートで生成するスプレッドシートに2枚のシートを作る方式にした。
| シート名 | 用途 |
|---|---|
| 事業者設定_エクスポート | 全年度の推移表(読み取り専用の参照用) |
| 事業者設定_インポート | 最新年度の設定値を縦に並べた編集用シート |
インポート用シートは「項目名 / 現在値 / 新しい値」の3列構成で、ユーザーが「新しい値」列を編集してからインポートを実行する。編集対象が明確になるので、全年度マトリクスの中から「どこを変えるのか」を探す手間が消えた。
インポート機能: 計画変更とUI実装
当初計画からの方針転換
最初はCLI(Claude Code経由)でインポートを実行する設計で進めていた。スプレッドシートのデータを読み取り、APIで設定値を更新するスクリプトを書く想定だった。
しかし途中で「Chrome拡張のUI上でインポートできた方が直感的」という判断に変わり、自動仕訳ルールタブと同じUIパターンで実装する方向に切り替えた。ドライラン表示→確認→実行という3ステップのフローを組む。
fetch APIによるフォームPOST
インポートの核心部分は、設定ページのフォームを fetch で直接POSTする方式を採った。ページ遷移が発生しないため、連続して複数の設定項目を更新できる。
// content scriptから設定ページのフォームをfetchで送信
const formData = new FormData();
formData.append('field_name', newValue);
formData.append('_method', 'patch');
formData.append('authenticity_token', token);
const res = await fetch('/offices', {
method: 'POST',
body: formData,
credentials: 'include'
});
authenticity_token はページ内の <meta> タグから取得する。セッションが切れていると403が返るため、レスポンスのステータスコードを見てエラーメッセージを出す処理も入れた。
ドライラン→同期実行フロー
インポートボタンを押すと、まずドライランが走る。スプレッドシートから読み取った「新しい値」と、現在の設定ページ上の値を比較して、差分がある項目だけをテーブルに表示する。
ユーザーが差分を目視確認して「同期実行」ボタンを押すと、差分のある項目だけをfetch POSTで更新する。全件完了後、再度設定ページを読み込んで結果を照合する。
ログ記録の統合
エクスポート/インポートのログ対応
事業者設定のエクスポートとインポートの実行結果を、既存のログタブに記録するようにした。ログ一覧から詳細を開くと、どの設定項目が更新されたか(エクスポートなら何年度分を出力したか)が確認できる。
yearsプロパティ欠損バグ
ログ詳細表示で「全年度結合表示」を行う箇所で、years プロパティが存在しないログレコードに対して .length を呼んでクラッシュするバグがあった。事業者設定エクスポートのログには years ではなく items という配列で記録していたため、プロパティ名の不一致が原因だった。
ログ詳細の表示ロジックに years ?? items のフォールバックを入れて、推移表パターンの colspan 計算と統一した。
周辺の修正
JSON設定エクスポートのファイル名にJST日時を追加
Chrome拡張の設定をJSONファイルとしてエクスポートする機能で、ファイル名に日時を含めていたが、new Date().toISOString() をそのまま使っていたためUTC時刻になっていた。深夜0時付近にエクスポートすると日付が前日になる。
JSTオフセット(+9時間)を加算した Date オブジェクトからファイル名を生成するように修正した。
// UTC → JST変換してファイル名に使用
const jst = new Date(Date.now() + 9 * 60 * 60 * 1000);
const ts = jst.toISOString().slice(0, 19).replace(/[T:]/g, '-');
const filename = `extension-settings-${ts}.json`;
サービス一覧エクスポートのマージ方式変更
サービス一覧のエクスポート処理で、スプレッドシートを毎回新規作成していたのをマージ方式に変更した。既存のスプレッドシートURLが設定されていれば、そのシートにデータを追記する。URLが未設定のときだけ新規作成する。
これにより、別のタブで参照しているスプレッドシートURLが実行のたびに変わってしまう問題が解消された。
設定画面から自動仕訳ルールURL欄を削除
自動仕訳ルールのスプレッドシートURLを設定画面の入力欄とchrome.storageの両方で管理していたが、chrome.storageを唯一の情報源(single source of truth)にする方針で入力欄を削除した。
エクスポート実行時にchrome.storageへURLを自動保存するフローが既に動いていたので、手動入力欄がなくても困らない。設定画面のUIがすっきりした分、タブ切替時の初期化コードも減った。
振り返り
朝の時点では「エクスポートだけ作って終わり」の予定だったが、エクスポート結果を見ているうちに「このまま書き戻せたら便利」という発想が湧いて、インポートまで一気に作り切った。ドライラン→確認→実行のフローは自動仕訳ルールタブで一度組んだパターンの流用なので、UIの配線は30分ほどで終わった。
Codexレビューを3回挟んだことで、エラーハンドリングの抜けを実装前に潰せたのは収穫だった。特に年度切替失敗時のマトリクス歯抜け問題は、手動テストだけでは気づきにくい。
fetch POSTでフォームを送信する方式は、ページ遷移なしで複数項目を連続更新できるため体感速度が段違いだった。10項目の更新が2秒で完了する。ページ遷移方式だと1項目ごとに3〜4秒かかるので、30秒以上の差が出る計算になる。