• #case100
  • #Vue
  • #Nuxt
  • #ハイドレーション
  • #ResizeObserver
開発eurekapu-nuxt4

夕方、デスクに戻ってエディタを開いた瞬間、中間JSON 79件のフォルダが目に入った。前のフェーズで Excel→TypeScript パイプラインを通して生成だけは済ませた論点群が、まだ Vue ページとして画面に出ていない状態でずっと放置されていた。今日のフェーズ3はこれを一気に画面に出す回。

まずは1論点を手で書いて地雷を踏む

いきなり一括変換スクリプトを書くと、隠れた地雷を踏んだとき原因が79倍に増える。最初は会計の参考書(ケース100書籍)の topic-1007 を手書きでプロトタイプ化することから始めた。意図としては「変換前にコンポーネント側のバグを先に出し切る」だった。

予想どおり、いきなり 500 エラーで落ちた。サーバーログに Cannot read properties of undefined (reading 'id') と出ている。JournalExample.vuelastFy.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 論点でカードのスタイルが微妙に違うので、共通コンポーネントに寄せる