夕方、デスクに戻ってエディタを開いた瞬間、中間JSON 79件のフォルダが目に入った。前のフェーズで Excel→TypeScript パイプラインを通して生成だけは済ませた論点群が、まだ Vue ページとして画面に出ていない状態でずっと放置されていた。今日のフェーズ3はこれを一気に画面に出す回。
まずは1論点を手で書いて地雷を踏む
いきなり一括変換スクリプトを書くと、隠れた地雷を踏んだとき原因が79倍に増える。最初は会計の参考書(ケース100書籍)の topic-1007 を手書きでプロトタイプ化することから始めた。意図としては「変換前にコンポーネント側のバグを先に出し切る」だった。
予想どおり、いきなり 500 エラーで落ちた。サーバーログに Cannot read properties of undefined (reading 'id') と出ている。JournalExample.vue が lastFy.entries[0].id を読んでいて、当期の entries が [] のときに添字 0 がない。今回の 79 論点には「当期に仕訳が発生しないシナリオ」がそこそこ混ざる予定だったので、ここで踏めたのはむしろ運がよかった。
Q1で「当面は entries0 を埋める」(A)と「コンポーネントを null-safe にする」(C)を比較して、Cの根本対応を選んだ。v-if="lastFy.entries[0]" を挟んで、空配列でもクラッシュしない形に書き直した。続いて Q2 で「共通前提の借入金などをどこに置くか」を決め、前期の preposted 側に寄せて当期は当該論点だけを動かす(B案)方針を確定。
その流れで topic-58(剰余金の配当)も先に通した。re(繰越利益剰余金)キーの集約表示、複数 entries のタブ、scenario の出し分け、純資産細分の集約 — このあたりが動くなら残り 76 論点もたぶん通る、というカンを得た。
79論点を一括出力する変換スクリプト
確信を得てから ts_to_vue.ts を書いた。中間 JSON の journals 構造を読んで、Vue ページデータと CHAPTERS インデックスを生成するだけのシンプルな関数群。意識したのは2点。
- 計算ロジック(科目集約、preposted 仕訳の差し込み、scenario 切り出し)は純粋関数に閉じる
- ファイル書き出しは末尾の薄いシェル1箇所だけにまとめる
走らせたら 79 件すべてエラーゼロで通った。インデックスページには既存の手書き 6 論点と新規 79 論点が章カテゴリ別にグルーピングされて並ぶ。自己株式や税効果(tax セクション)の複雑論点も画面で破綻しない。pnpm build も型エラーなしで完走した。
// 純粋関数。同じ JSON を渡せば同じ Vue データが出る
const toVuePageData = (j: JournalJson): VuePageData => ({
meta: pickMeta(j),
prepostedEntries: extractPreposted(j),
currentEntries: extractCurrent(j),
netAssetsKeys: collectNetAssetsKeys(j), // re 集約のため
})
レイアウト調整で時間を吸われた
機械的な移植が終わったあと、PC ワイド画面で見たときの息苦しさが気になった。仕訳カードがタブで重なって表示されているせいで、画面の右半分が広いのに情報密度が上がらない。isWide を判定して、ワイドなら縦並び、狭いならタブ形式、という分岐を入れた。
ここでハイドレーション mismatch を踏んだ。タブ切替も仕訳帳トグルも反応しない。原因は SSR 側の初期値を「ワイド」前提で書いたのに、クライアント側の最初のレンダリング時点では window が未定義で false 評価になっていたこと。コンソールに mismatch 警告が出ていた。SSR 初期値を「狭い画面前提」に統一して、onMounted 後に isWide を切り替える形に直したら、両環境ともクリック反応が戻った。
ResizeObserver で BS/PL を仕訳帳に追従させる
ワイドレイアウトで仕訳帳の開閉ボタンを押すと、左カラム(取引・仕訳候補)が一緒に伸縮してしまう違和感が出た。BS/PL パネルの高さに左カラムを合わせたいだけで、仕訳帳の開閉に巻き込まれたくない。ResizeObserver で BS/PL パネルの高さを観察して、CSS 変数として親に渡す方式に切り替えた。
const observer = new ResizeObserver((entries) => {
// contentRect.height は padding を含まない
const h = (entries[0].target as HTMLElement).offsetHeight
el.style.setProperty('--bs-pl-height', `${h}px`)
})
最初 entry.contentRect.height で取っていたら初期値だけ数pxズレた。初期値は offsetHeight(padding 含む)で計算していたので、ResizeObserver 側も offsetHeight を読み直して揃えた。align-items: start を付けて左右カラムが上端で揃うようにしたところで、ようやく仕訳帳を開閉しても左カラムが微動だにしなくなった。
ついでにもう1個踏んだ。仕訳帳を開いた瞬間に BS/PL の数値だけが消える現象。原因は左カラム側の上下に白背景が乗っていて、ワイド時に重なり合う領域で BS/PL を覆い隠していたこと。透明にして決着。
学びメモ
- 79 件を一括変換する前に、最初の1件で 500 エラーを2つ踏めたから、残り 78 件で同じ穴に落ちずに済んだ。プロトタイプは「動くもの」を作る作業ではなく、「壊れるもの」を作る作業だった
- ハイドレーション mismatch は、SSR 初期値とクライアント初期値の食い違いを読めば必ず再現する。今回は初期値を「狭い画面前提」に倒して mount 後に切り替える方針で揃えた
- ResizeObserver の
contentRect.heightは padding を含まない。初期値でoffsetHeightを使うなら ResizeObserver 側もoffsetHeightを読まないと数pxズレる - 機械的な変換が終わってからのレイアウト調整に予想の3倍時間を吸われた。一括移植のフェーズはコード生成より UI 仕上げのほうが長い
試行錯誤の記録
- 1FY 構成にしたら表示が通った → 2FY 構成(前期 preposted)に戻して再現確認 → コンポーネントを null-safe 化
- Nuxt が新規ファイルを認識しない → dev server を再起動
- タブが反応しない → コンソールに mismatch 警告 → SSR 初期値を狭い画面前提に修正
- BS/PL の数値が消える → 左カラムの白背景を透明化
明日やること
- 79 論点のうち、
taxセクションを含む複雑論点をいくつかピックアップして、scenario の出し分けが意図どおりかブラウザで再確認する - インデックスページの章グルーピングが多すぎて縦に長いので、章ごとに折りたたみを入れる
- 手書き 6 論点と新規 79 論点でカードのスタイルが微妙に違うので、共通コンポーネントに寄せる