ライブラリなしで全文検索つきCF実務リファレンスページを構築する
朝、CF(キャッシュ・フロー計算書)の論点一覧を眺めていて手が止まった。31論点・130キーワードを1ページに収めて、外部ライブラリなしで検索もフィルターも効かせたい。ブラウザのネイティブAPIだけで、どこまで実用に耐えるページが作れるか試してみた。
やったこと概要
- 2カラムレイアウトの新規ページ構築(左: stickyな論点ナビ、右: キーワード一覧)
- TursoDBから書籍のキーワードデータを取得し、31論点・130キーワードに拡充
- レベルタグ(3級/2級/実務)の付与とフィルター機能
- 基準条文のツールチップ/ポップオーバー実装
- 矢印キーによる論点間ナビゲーション
- スクロールバー幅ジャンプ問題の解決
- Codexレビューで条文の根拠間違いを5件修正
TursoDBからキーワードを引っ張る
最初は手書きで10論点ほど並べていたが、網羅性が足りない。TursoDBに格納済みの書籍データからCF関連キーワードを引いてきた。
SELECT topic, keyword, level
FROM book_keywords
WHERE subject = 'cf'
ORDER BY topic_order, keyword_order;
これで31論点・130キーワードが一括で取れた。レベル情報(3級/2級/実務)も書籍側で付与済みだったので、そのままフィルターUIのデータソースに使える。
2カラムレイアウトの設計
左カラムに論点ナビ、右カラムにキーワード一覧を配置。論点ナビはposition: stickyで画面上部に固定し、右をスクロールしても論点リストが常に見える。
<template>
<div class="reference-layout">
<nav class="topic-nav">
<!-- 31論点を2列で表示 -->
<ul class="topic-list two-columns">
<li v-for="topic in filteredTopics" ...>
{{ topic.label }}
</li>
</ul>
</nav>
<main class="keyword-panel">
<!-- 各論点のキーワードカード -->
</main>
</div>
</template>
左ナビは当初1列だったが、31論点が縦に並ぶとスクロールが発生する。2列化したら全論点がスクロールなしで視界に収まった。grid-template-columns: repeat(2, 1fr)で済む話だが、スクロールを消せた瞬間、ページの一覧性が一段上がった。
レベルフィルター
キーワードごとに3級/2級/実務のバッジを表示し、ヘッダーのトグルボタンでフィルターできる。computedで絞り込むだけのシンプルな実装。
const filteredKeywords = computed(() =>
keywords.value.filter(kw =>
activeFilters.value.includes(kw.level)
)
)
「3級だけ表示」にすると130件が40件ほどに絞られ、初学者が迷わない。
基準条文のポップオーバー
キーワードに紐づく会計基準の条文番号を表示する。最初はマウスオーバーでツールチップを出していたが、モバイルで使えない。クリックで展開するポップオーバーに切り替え、デスクトップではマウスオーバーでも開くようにした。
const showPopover = (e: Event, citation: string) => {
popoverContent.value = citation
popoverTarget.value = e.target as HTMLElement
isPopoverVisible.value = true
}
Popover APIを使いたかったが、Safari対応を考えて自前実装に落ち着いた。position: absoluteとgetBoundingClientRect()で位置を計算する古典的なパターン。
矢印キーナビゲーション
31論点をキーボードの上下矢印で移動できるようにした。論点を選択すると右カラムが該当セクションまでスムーススクロールする。
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault()
currentIndex.value = Math.min(currentIndex.value + 1, topics.length - 1)
scrollToTopic(currentIndex.value)
}
if (e.key === 'ArrowUp') {
e.preventDefault()
currentIndex.value = Math.max(currentIndex.value - 1, 0)
scrollToTopic(currentIndex.value)
}
}
キーボード操作を入れた途端、論点間を飛び回る速度が跳ね上がった。マウスでクリックしていたときは「次の論点」に0.5秒かかっていたのが、矢印キーなら連打で一瞬。
スクロールバー幅ジャンプ問題との格闘
フィルターを切り替えるたびにコンテンツ量が変わり、スクロールバーの出現/消失でページ全体が左右に2〜3pxジャンプする。地味だが、目に刺さる。
試行1: scrollbar-gutter
.keyword-panel {
scrollbar-gutter: stable;
}
Chromeでは効いたがFirefoxの挙動が微妙で、パネルの右端に常時スペースが空く見た目が気になった。
試行2: min-width: 0
.reference-layout {
min-width: 0;
}
Grid/Flexの子要素が暗黙の最小幅を持つ問題への対処。ジャンプは軽減されたが完全には消えない。
試行3: html要素にoverflow-y: scroll強制(採用)
html {
overflow-y: scroll;
}
常にスクロールバーを表示させることで、出現/消失自体を起こさない。最もシンプルで副作用がない。スクロールバーが常に見えること自体はユーザーに違和感を与えない(ほとんどのサイトはコンテンツがあるのでどのみち表示されている)。この1行で問題が消えた。
Codexレビューで条文の根拠間違いを修正
130キーワードに紐づけた基準条文の番号を、Codex rescueサブエージェントにレビューさせた。
codex exec -m gpt-5.4 "以下のCFキーワードと条文番号の対応を検証して。
間違いがあれば正しい条文番号を教えて: {file_path}"
5件の誤りが返ってきた:
| キーワード | 誤 | 正 |
|---|---|---|
| 利息の受取額 | 基準8項 | 基準9項 |
| 為替差損益 | 基準12項 | 実務指針3項 |
| 非資金損益項目 | 基準6項(2) | 基準14項(2) |
| 連結範囲の変動 | 基準30項 | 基準33項 |
| 重要な非資金取引 | 注記なし | 基準48項 |
人間が1つずつ基準書を引いて確認するより圧倒的に速い。ただし「Codexが正しいか」の最終検証は自分で基準書を開いて行った。AIが出した修正を鵜呑みにしない——この手順は省略できない。
全文検索の実装
外部ライブラリ(Fuse.js, MiniSearch等)を使わず、ネイティブのString.prototype.includes()で検索を回す。130件程度ならインデックス不要で瞬時に返る。
const searchResults = computed(() => {
if (!query.value) return allKeywords.value
const q = query.value.toLowerCase()
return allKeywords.value.filter(kw =>
kw.keyword.toLowerCase().includes(q) ||
kw.description.toLowerCase().includes(q)
)
})
入力のたびにフィルターが走るが、130件の文字列比較など1ms未満。ライブラリを入れる判断は「数千件を超えたとき」でいい。
振り返り
朝の「31論点を1ページに収めたい」から始めて、夕方にはフィルター・検索・キーボードナビ・条文ポップオーバーが揃ったページが動いていた。外部ライブラリゼロでここまで来れたのは、データ量が130件と少ないおかげ。
スクロールバーのジャンプ問題は3段階の試行錯誤を経てhtml { overflow-y: scroll }の1行に着地した。複雑な解決策から試して、最終的に最もシンプルな方法が勝つパターンは何度経験しても面白い。
Codexレビューで条文の誤りが5件見つかったのは収穫。自分の記憶だけで条文番号を書いていたら、気づかないまま公開していた。AIに「事実の検証」を委ねる運用が、こういうリファレンス系コンテンツでは特に効く。