• #Vue
  • #コンポーネント設計
  • #リファクタリング
  • #Miller Columns
  • #UI設計
開発eurekapuアクティブ

背景

eurekapu財務データSaaSでは、複数のタブでMiller Columns形式のUIを使用している。しかし、各タブで個別に実装されており、以下の問題が発生していた。

  • 同じような機能のコンポーネントが複数存在
  • 見た目や挙動に一貫性がない
  • 新機能追加時に同じ修正を複数箇所に適用する必要がある
  • コードベースが肥大化している

今回、7つのタブを調査し、Miller Columnsコンポーネントを3つのタイプに整理する計画を立てた。

調査対象の7タブ

以下の7つのタブを調査対象とした。

タブ名主な用途現状のコンポーネント
読取一覧OCR結果の一覧表示・編集独自実装
クレカ明細クレジットカード明細の表示独自実装
科目別一覧勘定科目ごとの取引一覧独自実装
帳票設定帳票テンプレートの設定DocumentTypeForm.vue
仕訳ルール自動仕訳ルールの管理独自実装
勘定科目マスター勘定科目の管理独自実装
月次推移表月次の財務推移表示独自実装

スクリーンショット撮影と分析

各タブのスクリーンショットを撮影し、UIパターンを分析した。

共通パターンの発見

撮影したスクリーンショットから、以下の共通パターンを発見した。

  1. 左ペイン: カテゴリやフィルタ条件の選択
  2. 中央ペイン: 一覧表示(テーブルまたはカード形式)
  3. 右ペイン: 詳細表示または編集フォーム

相違点

  • 左ペインの階層数(1階層〜3階層)
  • 選択モード(単一選択/複数選択)
  • 右ペインの有無
  • 編集機能の有無

3つのコンポーネントタイプの定義

分析結果から、以下の3タイプを定義した。

Type A: フィルタ型(MillerColumnsFilter)

用途: 一覧表示のフィルタリング

特徴:

  • 左ペインで条件を選択
  • 選択に応じて中央の一覧がフィルタリングされる
  • 右ペインは通常なし(または簡易プレビュー)

適用タブ:

  • 読取一覧
  • クレカ明細
  • 科目別一覧

インターフェース設計:

interface MillerColumnsFilterProps {
  // フィルタ階層の定義
  levels: FilterLevel[]
  // 選択されたフィルタ値
  selectedFilters: Record<string, string | string[]>
  // フィルタ変更時のコールバック
  onFilterChange: (filters: Record<string, string | string[]>) => void
}

interface FilterLevel {
  key: string
  label: string
  // 選択肢を取得する関数(前の階層の選択値に依存)
  getOptions: (parentSelection?: string) => FilterOption[]
  // 複数選択を許可するか
  multiple?: boolean
}

interface FilterOption {
  value: string
  label: string
  count?: number // 該当件数
}

Type B: 選択型(MillerColumnsSelect)

用途: マスターデータの選択・編集

特徴:

  • 左ペインで階層をドリルダウン
  • 中央ペインで項目を選択
  • 右ペインで選択項目の詳細表示・編集

適用タブ:

  • 勘定科目マスター
  • 仕訳ルール

インターフェース設計:

interface MillerColumnsSelectProps<T> {
  // 階層データ
  levels: SelectLevel<T>[]
  // 選択中の項目
  selectedItem: T | null
  // 選択変更時のコールバック
  onSelect: (item: T) => void
  // 編集可能か
  editable?: boolean
  // 編集時のコールバック
  onEdit?: (item: T) => void
}

interface SelectLevel<T> {
  key: string
  label: string
  // 階層データを取得
  getData: (parentId?: string) => T[]
  // 表示用のレンダラー
  renderItem: (item: T) => VNode
}

Type C: 詳細表示型(MillerColumnsDetail)

用途: 設定画面や詳細表示

特徴:

  • 左ペインでカテゴリを選択
  • 右ペインに詳細フォームや設定画面を表示
  • 中央ペインはオプション

適用タブ:

  • 帳票設定
  • 月次推移表

インターフェース設計:

interface MillerColumnsDetailProps {
  // カテゴリ一覧
  categories: Category[]
  // 選択中のカテゴリ
  selectedCategory: string | null
  // カテゴリ選択時のコールバック
  onCategorySelect: (categoryId: string) => void
  // 詳細コンテンツのスロット
  detailSlot: VNode
}

interface Category {
  id: string
  label: string
  icon?: string
  badge?: string | number
}

既存コンポーネントの問題点

重複実装

同じ機能が複数のコンポーネントに実装されていた。

components/
├── DocumentTypeForm.vue      # 帳票設定用(Type C相当)
├── DocumentTypeCard.vue      # カード表示(未使用)
├── FilterPanel.vue           # 読取一覧用フィルタ
├── FilterPanelCredit.vue     # クレカ明細用フィルタ(ほぼ同じ)
├── AccountSelect.vue         # 科目選択
├── AccountSelectModal.vue    # モーダル版科目選択
└── ...

一貫性のなさ

問題点
スタイルの不統一ボーダー色、パディング、フォントサイズが異なる
APIの不統一v-model / @select / @change が混在
状態管理の不統一ローカル状態 / Pinia / props経由が混在

未使用コンポーネントの存在

調査の結果、以下のコンポーネントが未使用であることが判明した。

  • DocumentTypeForm.vue: 古い帳票設定フォーム
  • DocumentTypeCard.vue: 使われていないカード表示

Codexレビューの活用

計画の妥当性を確認するため、Codex(GPT-5.2)に2回レビューを依頼した。

第1回レビュー:タイプ分類の妥当性

レビュー依頼内容:

  • 3タイプへの分類は適切か
  • 他に考慮すべきパターンはあるか

レビュー結果:

  • 3タイプ分類は妥当
  • Type A と Type B の境界が曖昧なケースがあるため、判断基準を明確にすべき
  • 「フィルタリング目的か、選択・編集目的か」で判断することを推奨

第2回レビュー:インターフェース設計

レビュー依頼内容:

  • 各タイプのインターフェース設計は適切か
  • Vue 3 Composition API との相性は良いか

レビュー結果:

  • ジェネリクスの使用は適切
  • onXxx 形式のコールバックより emit を使う Vue 慣習に合わせるべき
  • composables/ にロジックを切り出すことを推奨

未使用コンポーネントの削除

調査結果を踏まえ、以下のコンポーネントを削除した。

削除対象

# 削除したファイル
rm components/DocumentTypeForm.vue
rm components/DocumentTypeCard.vue

削除理由

ファイル削除理由
DocumentTypeForm.vue新しい帳票設定画面に置き換え済み
DocumentTypeCard.vueどこからも参照されていない

削除前の確認

削除前に以下を確認した。

# 参照箇所の確認
grep -r "DocumentTypeForm" --include="*.vue" --include="*.ts"
grep -r "DocumentTypeCard" --include="*.vue" --include="*.ts"

結果、どちらも参照箇所がなかったため削除を実行した。

今後の作業計画

Phase 1: 基盤コンポーネントの作成

  1. MillerColumnsBase.vue の作成(共通ロジック)
  2. useMillerColumns composable の作成
  3. 共通スタイルの定義(miller-columns.css

Phase 2: Type A(フィルタ型)の実装

  1. MillerColumnsFilter.vue の作成
  2. 読取一覧への適用
  3. クレカ明細への適用
  4. 科目別一覧への適用

Phase 3: Type B(選択型)の実装

  1. MillerColumnsSelect.vue の作成
  2. 勘定科目マスターへの適用
  3. 仕訳ルールへの適用

Phase 4: Type C(詳細表示型)の実装

  1. MillerColumnsDetail.vue の作成
  2. 帳票設定への適用
  3. 月次推移表への適用

Phase 5: クリーンアップ

  1. 旧コンポーネントの削除
  2. テストの追加
  3. ドキュメントの整備

設計上の決定事項

命名規則

MillerColumns + [Type] + [Variant]
例: MillerColumnsFilterCompact, MillerColumnsSelectTree

ディレクトリ構造

components/
└── miller-columns/
    ├── MillerColumnsBase.vue
    ├── MillerColumnsFilter.vue
    ├── MillerColumnsSelect.vue
    ├── MillerColumnsDetail.vue
    ├── parts/
    │   ├── ColumnPane.vue
    │   ├── ColumnItem.vue
    │   └── ColumnHeader.vue
    └── composables/
        ├── useMillerColumns.ts
        ├── useColumnNavigation.ts
        └── useColumnSelection.ts

スタイル方針

  • Tailwind CSS を使用
  • 共通のデザイントークンを定義
  • ダークモード対応

まとめ

7つのタブを調査し、Miller Columnsコンポーネントを3タイプに整理する計画を立てた。

  • Type A(フィルタ型): 読取一覧、クレカ明細、科目別一覧
  • Type B(選択型): 勘定科目マスター、仕訳ルール
  • Type C(詳細表示型): 帳票設定、月次推移表

未使用コンポーネント(DocumentTypeForm.vue、DocumentTypeCard.vue)は削除済み。Codexレビューで設計の妥当性も確認した。

今後はPhase 1から順次実装を進める。