概要
tax-assistantのマルチクライアント対応を、設計フェーズから実装完了まで一気に進めた。 全10タスクを順番に消化し、バックエンド・フロントエンドの両方を改修。 その後、2つ目のクライアント(クライアントB)を追加した途端に隠れていたバグが5件表面化し、それらも全て修正した。
実装の全体像
1. dependencies.pyでクライアントパス管理を集約
マルチクライアント対応の中核となる依存関数 get_client_paths() を新設した。
- リクエストヘッダーの
X-Client-IdからクライアントIDを取得 - クライアント固有のDBパス・データパスを解決して返す
- パストラバーサル攻撃を防ぐセキュリティチェックを組み込み
# dependencies.py の概要
def get_client_paths(request: Request) -> ClientPaths:
client_id = request.headers.get("X-Client-Id", "client_001")
# パストラバーサル防止
if ".." in client_id or "/" in client_id:
raise HTTPException(status_code=400, detail="Invalid client ID")
return ClientPaths(
db_path=BASE_DIR / "clients" / client_id / "main.db",
data_path=BASE_DIR / "clients" / client_id / "data",
)
この設計により、各ルーターはパス解決を意識せず db_path を受け取るだけでよくなった。
2. クライアント一覧API + main.pyの修正
/api/clientsエンドポイントを追加。clients/ディレクトリ配下のサブディレクトリ一覧を返す- main.pyの
lifespanからDB初期化ロジックを削除し、依存関数側に移行
これにより、アプリ起動時に特定クライアントのDBをハードコードで初期化する必要がなくなった。
3. 全ルーターの移行(10ファイル)
ルーター移行は2段階で進めた。
単純なルーター(6ファイル):
- square
- creditcard
- comments
- validations
- account_categories
- shiwake_rules
これらは DB_PATH のハードコードを db_path: Path = Depends(get_client_paths) に置き換えるだけで済んだ。
複雑なルーター(4ファイル):
- receipts(ファイルパス参照あり)
- batches(バッチ処理でDB接続を複数回開く)
- journal(仕訳生成ロジックが長い)
- document_types(マスターデータ参照の依存関係)
複雑なルーターは、DB接続だけでなくデータディレクトリの参照もクライアント別にする必要があった。
4. フロントエンド対応
api.ts: useApi()にヘッダー自動注入
// api.ts
export const useApi = () => {
const clientStore = useClientStore()
const headers = computed(() => ({
"X-Client-Id": clientStore.currentClientId,
}))
// ... fetch wrapper with headers
}
Piniaストア: useClientStore
- クライアント一覧の取得・キャッシュ
- 選択中のクライアントIDを
localStorageで永続化 - クライアント切替時にデータ再取得をトリガー
ClientSelectorコンポーネント
ヘッダー左上にドロップダウンを配置。選択を変えると即座にクライアントが切り替わる。
2つ目のクライアント追加で表面化したバグ
クライアントA(既存)だけの状態では気づけなかった問題が、クライアントBを追加した途端に5件発覚した。
バグ1: ネイティブfetch()によるヘッダー未送信
仕訳ルール関連の5コンポーネントが useApi() ではなくネイティブの fetch() を直接使っていた。
useApi() 経由でないと X-Client-Id ヘッダーが付与されないため、常にデフォルトのクライアントAのデータを返していた。
修正: 該当5コンポーネントを全て useApi() 経由に統一。
バグ2: 新クライアントの勘定科目マスターが0件
クライアントBのDBを新規作成した際、テーブルは作られるがマスターデータが空だった。 勘定科目が0件だと仕訳生成が全く動作しない。
修正: クライアントAの勘定科目マスター174件をクライアントBにコピーする初期化処理を追加。
バグ3: 仕訳ビューで税区分が表示されない
account_categories の検索ロジックに問題があった。勘定科目コードの完全一致ではなく前方一致で検索していたため、クライアントBのデータ構造で正しくヒットしなかった。
修正: 検索ロジックを完全一致に修正。
バグ4: レシートなしのクライアントでルールマッチング全スキップ
レシートが存在しないクライアントでは、仕訳ルールのマッチング処理が丸ごとスキップされていた。 ルールマッチングはレシート以外のデータ(クレジットカード明細など)にも適用すべきだった。
修正: unchecked ステータスのレコードにもマッチング処理を適用するよう変更。結果、全534件中214件がルールマッチに成功。
バグ5: fetch()とuseApi()の混在箇所の洗い出し
バグ1の発見を受けて全コンポーネントを横断検索したところ、仕訳ルール関連以外にも数箇所ネイティブ fetch() が残っていた。
修正: 全箇所を useApi() に統一。
学んだこと
2つ目のクライアントを追加すると隠れた問題が表面化する。 1クライアントだけだとハードコードされたパスやデフォルト値が「たまたま動く」状態になりがちで、マルチテナント化の際に初めて問題が顕在化する。
ネイティブfetch()とラッパー関数の混在は危険。 ヘッダー注入・認証トークン付与など、ラッパーに集約した共通処理が漏れる原因になる。lintルールやコードレビューで fetch() 直接呼び出しを検知する仕組みがあるとよい。
クライアント初回セットアップ時のマスターデータコピーは忘れがち。 テーブル作成(マイグレーション)だけでなく、初期データの投入(シーディング)まで自動化しておく必要がある。
まとめ
| 項目 | 数値 |
|---|---|
| 移行したルーター | 10ファイル |
| 対応エンドポイント数 | 59 |
| 発見・修正したバグ | 5件 |
| ルールマッチ結果 | 534件中214件 |
設計が済んでいたおかげで、実装自体はスムーズに進んだ。ただし「動くようになった」と思った後に2つ目のクライアントを追加してからが本番だった。マルチテナント対応は実装よりも検証フェーズで問題が出やすいということを改めて実感した。