• #Chrome拡張機能
  • #クラウド会計
  • #UI設計
  • #ミラーカラム
  • #chrome.storage
  • #リファクタリング
開発tax-assistantメモ

Chrome拡張 会計サービス連携 - エクスポートログとUIリファクタリング

エクスポート処理を走らせるたびに「前回どの事業者のどの年度まで終わったか」を忘れる問題を、ログ機能で解決した。ついでにUIをモーダル構造からミラーカラム3列に組み替え、タブ構造も再構成した。CTI切替時のサービスリスト取得問題やDOMセレクタの罠にはまりながらの一日。


エクスポートログ機能の追加

動機

複数の事業者 x サービス x 年度の組み合わせでエクスポートを回すと、どこまで成功してどこでエラーが出たのかが記憶から消える。毎回DevToolsのコンソールを遡るのも限界がある。

設計

事業者×サービス×年度のマトリクスを1回のエクスポート実行につき1レコードとして chrome.storage.local に保存する構造にした。

// ログ1件のデータ構造(概要)
{
  id: crypto.randomUUID(),
  timestamp: Date.now(),
  entityName: '事業者A',
  matrix: {
    // service -> year -> status
    '連携明細': { '2024': 'success', '2025': 'error' },
    '仕訳帳':  { '2024': 'success', '2025': 'skip' }
  }
}

ログ一覧はタイムスタンプ降順で表示し、各行をクリックすると詳細マトリクスが右カラムに展開される。


リアルタイムマトリクス表示

エクスポート実行中、マトリクステーブルの各セルがpending → success / error / skip へ遷移するたびにポコポコ色が変わる。これが見ていて思いのほか気持ちいい。

ステータス遷移はCSSのpopアニメーションで表現した。セルが一瞬膨らんで色が切り替わる。

@keyframes cell-pop {
  0% { transform: scale(1); }
  50% { transform: scale(1.15); }
  100% { transform: scale(1); }
}
.matrix-cell.updated {
  animation: cell-pop 0.3s ease-out;
}

pending状態は薄いグレー、successは緑、errorは赤、skipは黄色。エクスポートが進むにつれてテーブルが緑に染まっていく様子が進捗インジケータの役割も果たす。


モーダルからミラーカラム3列へ

モーダルの限界

ログ詳細を最初はモーダルで表示していた。しかしログ一覧とマトリクス詳細を行き来するたびにモーダルの開閉が発生し、文脈が途切れる。特にエラーの多いログを連続で確認するとき、クリック→モーダル表示→閉じる→次のログ→モーダル表示のループがストレスだった。

ミラーカラムへの移行

mdx-playgroundで実装したミラーカラムパターンを参照し、3カラム構成に変更した。

┌──────────┬──────────────┬────────────────────┐
│  ナビ    │  ログ一覧    │  詳細マトリクス     │
│(タブ)  │(日時順)    │(事業者×年度×結果) │
└──────────┴──────────────┴────────────────────┘

ログ一覧で行を選択すると、右カラムにマトリクスが即座に切り替わる。モーダルを閉じる操作がなくなり、エラー確認の速度が上がった。3つの情報が同時に視界に入るのは、モーダル方式では実現できなかった利点。


タブ構造の再構成

ポップアップのタブが機能追加のたびに増殖して混沌としていたので、大カテゴリとサブカテゴリの2階層に整理した。

大カテゴリ: 設定 / エクスポート / インポート

エクスポート内のサブカテゴリ: 連携明細 / 仕訳帳 / ログ

これまでは設定タブがデフォルトで開いていたが、実際に一番使うのはエクスポートなので、デフォルトタブをエクスポートに変更した。設定は初回だけ触って以後ほぼ開かない。


事業者ネスト表示UI

連携明細と仕訳帳で、事業者カードの中にチェックボックスをネストする構造に統一した。

連携明細: 事業者カード内に年度とサービスの2段ネスト。事業者→年度→サービスチェックボックスの階層。

仕訳帳: 事業者カード内に年度チェックボックス。

当初は連携明細のみサービスチェックボックスのフラット表示だったが、年度ごとに取得対象が異なるケースに対応できず、2段ネストに変更。仕訳帳側と見た目が揃い、ユーザーのメンタルモデルが統一された。


サービスリストのfetch問題 - CTI切替が必要だった

ここで半日はまった。

症状

事業者一括エクスポート時に、2番目以降の事業者でサービスリスト(銀行口座やカード等の連携先)が取得できない。1番目の事業者は正常に表示されるのに、2番目以降では空配列が返る。

原因を掘る

サービスリストは会計サービスの特定ページ(accountsページ)のDOM内に埋め込まれている。しかしaccountsページを開いていない状態ではDOMにサービス情報が存在しない。

ログを追うと、事業者を切り替える(CTI切替のPATCHリクエスト)だけではサービスリストが更新されないことがわかった。CTI切替後に、改めてaccountsページをfetchしてHTMLからサービスリストをパースする必要がある。

解決

一括取得の処理フローを以下の順序に修正した。

事業者ごとのループ:
  1. CTI切替(PATCHリクエスト)
  2. accountsページをfetch → サービスリストをパース
  3. 各サービスのエクスポート処理を実行
  4. 次の事業者へ

2のステップが抜けていたのが原因。CTI切替だけでセッションのコンテキストは変わるが、サービスリストの「取得」は別途必要という、言われてみれば当然の話だった。


DOMセレクタの罠とレジストリ参照への切替

インポートパネルのコンテキストバー

インポート画面にも「現在の事業者名と年度」を表示するコンテキストバーを追加した。事業者名の取得に getEntityName() 関数を使っていたが、ここで問題が起きた。

DOMセレクタが壊れる

getEntityName() は会計サービスのヘッダー内のDOM要素からテキストを取得していた。しかしChrome拡張のポップアップからこの関数を呼ぶと、ポップアップのDOMと会計サービスページのDOMは別のコンテキストなので、セレクタが何もヒットしない。

content scriptからメッセージパッシングで取得する手もあったが、会計サービスのページが開いていないケースでは使えない。

レジストリから取得に変更

事業者一覧取得時にchrome.storageへ保存しているレジストリ(事業者名・CTI・年度の対照表)が既にある。DOMを直接覗くのをやめて、レジストリのデータを参照する方式に変更した。

// Before: DOMセレクタ(ポップアップからは動かない)
const name = document.querySelector('.entity-name')?.textContent;

// After: レジストリ参照(どこからでも取得可能)
const entity = await getRegistryEntry(currentCti);
const name = entity?.name ?? '不明';

これで会計サービスのページが開いていなくても事業者名を表示できるようになった。DOM依存を減らせたのは副次的な成果。


デッドコード削除とモジュール分離

機能追加が一段落したところでコードを整理した。

  • fetchJournalCsv など使われなくなった3関数を削除
  • resolveJournalToRows をメインファイルから lib.js に分離
  • URL未設定の事業者のチェックボックスを disabled 化(エクスポートしようがないので)
  • チェック状態をchrome.storageに保存・復元する処理を追加

振り返り

一日を通して触った範囲が広い。ログ機能、マトリクス表示、ミラーカラムUI、タブ再構成、ネストUI、フェッチフロー修正、DOMセレクタ問題、デッドコード削除。

個々の変更は小さいが、「データをどこに保存して、どこから参照するか」という判断が繰り返し問われた日だった。DOMセレクタ→レジストリ参照の変更が典型で、「取得元を間接参照に切り替える」という同じパターンが、サービスリスト問題でも事業者名問題でも登場した。

マトリクスのセルがポコポコ埋まっていくアニメーションは、ログ機能としての実用性以上に「ちゃんと動いている」安心感を生んでいる。進捗の可視化は小さな投資で大きく効く。