• #簿記3級
  • #Vue
  • #決算振替
  • #T勘定
  • #Codexレビュー
開発eurekapu-nuxt4

簿記3級の equity 例題C「決算振替仕訳」を既存の JournalExample.vue に乗せようとして、3時間粘ってから諦めた。期中仕訳と決算整理仕訳と決算振替仕訳が混ざる構造を1つのコンポーネントで表現しようとすると、props と分岐がツリー状に膨らんで、テストの意図が読めなくなる。別物として ClosingTransferExample.vue を切り出したら、計画→実装→Codexレビュー→アニメ追加まで1日で着地した。

やったこと(時系列)

朝、equity 例題Cの画面を開いて手が止まった。c5(収益→損益)、c6(費用→損益)、c7(損益→繰越利益剰余金)の3本が「決算振替仕訳」で、c1〜c4 の期中取引とは性質がまるで違う。JournalExample.vue に決算振替モードを追加するか別物として切り出すかを15分くらい悩んで、計画書を memo/2026-05-03/closing-transfer-example-component-plan.md に書き出した。

計画書ができたら Codex(GPT-5.5)に2ラウンドでレビューを回した。1ラウンド目で7件、2ラウンド目で4件の致命指摘が返ってきて、全部反映してから承認をもらった。指摘の中で重かったのは「c5を取り消したらc6・c7のT勘定への加算も巻き戻さないと整合性が崩れる」という連鎖取消の話で、設計を「連鎖取消・再構築方式」に書き換えた。

連鎖取消・再構築方式:c5取消でc6・c7も自動取消

決算振替仕訳は依存関係が強い。c5(売上→損益)が消えたのに、c7(損益→繰越利益剰余金)が損益5,000のまま残っていたら、T勘定が破綻する。

実装方針は単純で、「取消されたエントリーより後ろを全部破棄して再構築する」。c5を取り消したら c6・c7 も巻き戻し、T勘定を初期状態から c1〜c4 まで再生する。これで純粋関数 buildTLedgerStateView(entries) だけで状態が一意に決まる。

const canUnpostEntry = (entryId: string, allEntries: Entry[]): boolean => {
  // c1〜c4 は期中取引なので取消禁止
  const entry = allEntries.find(e => e.id === entryId)
  if (entry?.phase === 'operating') return false
  return true
}

const unpostEntry = (entryId: string, posted: Entry[]): Entry[] => {
  const idx = posted.findIndex(e => e.id === entryId)
  return idx === -1 ? posted : posted.slice(0, idx)
}

純粋関数に閉じ込めたので、Vitest で取消後のT勘定残高をスナップショット検証できる。Codex指摘で「c1〜c4 取消禁止のテストが無い」と言われて、後から3本追加した。

T勘定をBS/PL2段構成に:タブナンバリングは 1 2 3 4 |決算整理| 5 6 7

T勘定の並びを最初は勘定科目順にしていたが、決算振替の流れが目で追えない。BS(資産→負債→純資産)/ PL(収益→費用)の2段構成に組み替えたら、c5・c6 の「PL勘定が損益勘定に集約される動き」と c7 の「損益勘定が繰越利益剰余金に流れる動き」が画面上で1直線に見えるようになった。

タブナンバリングは仕訳7本ぶん 1 2 3 4 |決算整理| 5 6 7 という並び。決算整理セパレーターはマゼンタの縦線で、期中と決算をひと目で区切る。Codex には「セパレーターを文字で書くか線で書くか」を聞いて、線のほうがクリック可能領域と誤認されないと判定された。

縦並びとタブの差し戻し:最初は縦1列、モーダル拡大時のみ縦並びに

最初の実装で「縦1列のシンプル並び」にしたら、ユーザーから差し戻しが入った。「7枚のカードが縦に並んでいると、c1〜c4 の期中取引が長くて c5〜c7 まで視線が届かない。タブで集約して」。

通常時は1枚ずつタブ集約、モーダル拡大時のみ縦並び一覧という二段構えに変更。モーダルでは7枚を全部見渡せて、通常時はタブで切り替えて1枚に集中できる。差し戻しから実装変更まで30分。最初に勝手に「縦並びがシンプルでよいだろう」と決めつけたのが失敗で、モードを2つ持たせるべきだった。

レイアウトシフト0px:T勘定と仕訳帳をプレースホルダーで事前確保

昨日の JournalExample.vue で得た学びをそのまま流用した。T勘定の高さは「最終行数」で事前確保し、空行をプレースホルダーで表示する。仕訳帳も全7エントリー分のプレースホルダー行を最初から確保する。

これでアニメ着弾時にレイアウトが1pxも動かない。pillが飛んできたあとにカウントアップ表示が起動して、プレースホルダーが実データに置き換わる。

pill飛行アニメの座標バグ:3コミ目から座標がズレる

仕訳→T勘定への飛行pillアニメを実装した。c1・c2 までは綺麗に飛ぶのに、c3 以降から着弾点が右下にズレる。3回試行錯誤した。

原因はモーダルの zoom 倍率と左カラムのスクロール量だった。document.documentElement.style.zoom = '130%' で全体を拡大しているので、getBoundingClientRect() は zoom 後の座標を返す。一方、pillの translate 値は zoom 前の論理座標で書いていた。さらにモーダル左カラムが内部でスクロールするので、c3 を発火する時点でスクロール量が0pxではない。

// 修正後: zoom倍率を逆算してスクロール量を加味
const zoom = parseFloat(getComputedStyle(modalRoot).zoom || '1')
const fromRect = pillEl.getBoundingClientRect()
const toRect = targetCellEl.getBoundingClientRect()
const dx = (toRect.left - fromRect.left) / zoom
const dy = (toRect.top - fromRect.top) / zoom

getBoundingClientRect() の返値同士の差分を取ってから zoom で割る、と書いたら3コミ目以降も追従した。最初は zoom を transform: scale() に変えようとしたが、座標計算がさらに複雑になるので zoom のまま倍率逆算で対処した。

残高6,000のバグ:「対借が一致しません」表示の許容できない不整合

c5・c6・c7 を全部プッシュした状態で画面を開いたとき、ユーザーから「残高6,000の表示で対借が一致しない。これは許容できない」と指摘が入った。

最初は計算ロジックのバグかと思って buildTLedgerStateView を疑った。テストを書いて回したら、純粋関数の出力は正しい。T勘定の借方に期首残高(前期繰越)が表示されていなかっただけの、表示側のバグだった。データは合っていて、画面に出していなかった。

// 修正後: 期首残高を借方の最初の行に「前期繰越」として表示
const renderDebitColumn = (account: Account, openingBalance: number) => {
  const lines: TLedgerLine[] = []
  if (openingBalance > 0) {
    lines.push({ label: '前期繰越', amount: openingBalance })
  }
  return [...lines, ...account.debitEntries]
}

画面に出ている数字を電卓で検算したわけではなく、画面の貸借合計が違和感のある数字だったから気付いた。AI が書く表示ロジックは、計算と描画のどっちで欠けても結果が同じに見えることがある。テストで保証していなかった残高表示のT勘定 line 数まで合計17本のテストを追加した(7→10→17)。

Codexレビューで2点重要指摘:STEP 3 一時誤表示と c1〜c4 取消禁止テスト

実装が一段落して Codex に再レビューを回したら、瑣末な指摘を切り捨てたあとに2点が残った。

1つ目は「STEP 3 一時誤表示」。c6 を押下した直後の1.3秒間、画面に「当期純利益=5,000」と一時的に出ていた。c6 の時点では損益勘定の貸借差額が確定していないので、表示してはいけない値だった。showStep3 フラグの立ち上げタイミングが c6 の処理中に挟まっていて、「c7 を押下したあと」に変える必要があった。これも純粋関数 shouldShowStep3(entries) に切り出して、テストで「c6 直後は false」「c7 直後は true」を明示した。

2つ目は前述の「c1〜c4 取消禁止のテスト未追加」。canUnpostEntry の実装はあったがテストが無かった。3本追加した。

両方とも実装の自己テストでは見つからず、Codex の第三者レビューで初めて言語化された。指摘の半分は瑣末なので切り捨てるが、残り半分が致命点に当たる確率が高い。

細かい仕上げ:決算整理仕訳フラグ列、損益貸方山のパルス表示

仕上げに2つ追加した。仕訳帳の貸方右に「決算整理仕訳フラグ列」を立てて、c5〜c7 にだけマゼンタの「決」アイコンを表示する。期中と決算のリズムが仕訳帳上でも見える。

もう1つは損益勘定の貸方山のパルス表示。c5 でプッシュされた売上5,000 が損益勘定の貸方に積まれた瞬間、その金額にパルスとフェードイン効果を出す。「ここに数字が積まれた」という視覚的な合図で、c7 で繰越利益剰余金に流れていく前段の溜まりを強調する。

学び

  • 既存コンポーネントを変に拡張せず、別物として切り出す判断が結果的に早い: JournalExample.vue に決算振替モードを足そうとして3時間溶かしたが、ClosingTransferExample.vue を新規で切り出してから1日で完走した。「props で分岐」が3階層を超えたら別物として書く
  • 画面の数字に違和感を覚えた瞬間、テストで保証する: 残高6,000のバグは電卓で検算したのではなく、画面の貸借合計が変だと目で気付いた。気付いた瞬間にテストを書き足して、再発を防ぐ
  • 純粋関数化は連鎖取消・タイミングバグ両方に効く: buildTLedgerStateView canUnpostEntry shouldShowStep3 の3つを純粋関数に切り出したことで、状態が引数だけで一意に決まり、Codex指摘のタイミングバグもテストで言語化できた
  • Codexの指摘は瑣末を切り捨てれば致命点だけ残る: 11件の指摘から瑣末を半分切り、残った2点が STEP 3 誤表示と取消禁止テスト未追加。第三者レビューで初めて言語化される設計穴がある

明日やること

  • ClosingTransferExample.vue を他章(商品売買・固定資産)の決算振替に流用できるか、props 設計を見直す
  • 損益勘定の貸方山のパルス表示が連続発火するとフレームが落ちるので、Web Animations API の composite: 'add' で重ね合わせる方式に変える
  • 整合性保証テストを17本→各章ごとに展開して、決算振替系全例題で同じ純粋関数を回せる構造にする