[{"data":1,"prerenderedAt":536},["ShallowReactive",2],{"content-/2026-05-03-case100-vue-migration":3,"all-pages-for-dir":534,"og-image-/2026-05-03-case100-vue-migration":535},{"id":4,"title":5,"body":6,"category":517,"description":518,"extension":519,"meta":520,"navigation":487,"path":521,"project_name":522,"published":523,"publishedAt":524,"seo":525,"stem":526,"tags":527,"todo":532,"updatedAt":532,"__hash__":533},"pages/2026-05/2026-05-03/case100-vue-migration.md","case100 書籍79論点をVueページに一括移植。レスポンシブと仕訳帳UIを一緒に整える",{"type":7,"value":8,"toc":508},"minimark",[9,13,17,20,43,59,66,70,81,91,102,241,244,251,269,273,276,408,426,429,432,456,459,473,476,505],[10,11,12],"p",{},"夕方、デスクに戻ってエディタを開いた瞬間、中間JSON 79件のフォルダが目に入った。前のフェーズで Excel→TypeScript パイプラインを通して生成だけは済ませた論点群が、まだ Vue ページとして画面に出ていない状態でずっと放置されていた。今日のフェーズ3はこれを一気に画面に出す回。",[14,15,16],"h2",{"id":16},"まずは1論点を手で書いて地雷を踏む",[10,18,19],{},"いきなり一括変換スクリプトを書くと、隠れた地雷を踏んだとき原因が79倍に増える。最初は会計の参考書（ケース100書籍）の topic-1007 を手書きでプロトタイプ化することから始めた。意図としては「変換前にコンポーネント側のバグを先に出し切る」だった。",[10,21,22,23,27,28,31,32,35,36,31,39,42],{},"予想どおり、いきなり 500 エラーで落ちた。サーバーログに ",[24,25,26],"code",{},"Cannot read properties of undefined (reading 'id')"," と出ている。",[24,29,30],{},"JournalExample.vue"," が ",[24,33,34],{},"lastFy.entries[0].id"," を読んでいて、当期の ",[24,37,38],{},"entries",[24,40,41],{},"[]"," のときに添字 0 がない。今回の 79 論点には「当期に仕訳が発生しないシナリオ」がそこそこ混ざる予定だったので、ここで踏めたのはむしろ運がよかった。",[10,44,45,46,50,51,54,55,58],{},"Q1で「当面は entries",[47,48,49],"span",{},"0"," を埋める」（A）と「コンポーネントを null-safe にする」（C）を比較して、Cの根本対応を選んだ。",[24,52,53],{},"v-if=\"lastFy.entries[0]\""," を挟んで、空配列でもクラッシュしない形に書き直した。続いて Q2 で「共通前提の借入金などをどこに置くか」を決め、前期の ",[24,56,57],{},"preposted"," 側に寄せて当期は当該論点だけを動かす（B案）方針を確定。",[10,60,61,62,65],{},"その流れで topic-58（剰余金の配当）も先に通した。",[24,63,64],{},"re","（繰越利益剰余金）キーの集約表示、複数 entries のタブ、scenario の出し分け、純資産細分の集約 — このあたりが動くなら残り 76 論点もたぶん通る、というカンを得た。",[14,67,69],{"id":68},"_79論点を一括出力する変換スクリプト","79論点を一括出力する変換スクリプト",[10,71,72,73,76,77,80],{},"確信を得てから ",[24,74,75],{},"ts_to_vue.ts"," を書いた。中間 JSON の ",[24,78,79],{},"journals"," 構造を読んで、Vue ページデータと CHAPTERS インデックスを生成するだけのシンプルな関数群。意識したのは2点。",[82,83,84,88],"ul",{},[85,86,87],"li",{},"計算ロジック（科目集約、preposted 仕訳の差し込み、scenario 切り出し）は純粋関数に閉じる",[85,89,90],{},"ファイル書き出しは末尾の薄いシェル1箇所だけにまとめる",[10,92,93,94,97,98,101],{},"走らせたら 79 件すべてエラーゼロで通った。インデックスページには既存の手書き 6 論点と新規 79 論点が章カテゴリ別にグルーピングされて並ぶ。自己株式や税効果（",[24,95,96],{},"tax"," セクション）の複雑論点も画面で破綻しない。",[24,99,100],{},"pnpm build"," も型エラーなしで完走した。",[103,104,109],"pre",{"className":105,"code":106,"language":107,"meta":108,"style":108},"language-ts shiki shiki-themes vitesse-light vitesse-light","// 純粋関数。同じ JSON を渡せば同じ Vue データが出る\nconst toVuePageData = (j: JournalJson): VuePageData => ({\n  meta: pickMeta(j),\n  prepostedEntries: extractPreposted(j),\n  currentEntries: extractCurrent(j),\n  netAssetsKeys: collectNetAssetsKeys(j), // re 集約のため\n})\n","ts","",[24,110,111,119,160,180,197,214,235],{"__ignoreMap":108},[47,112,115],{"class":113,"line":114},"line",1,[47,116,118],{"class":117},"sxvE3","// 純粋関数。同じ JSON を渡せば同じ Vue データが出る\n",[47,120,122,126,130,134,137,141,144,148,151,154,157],{"class":113,"line":121},2,[47,123,125],{"class":124},"stQ0i","const ",[47,127,129],{"class":128},"senZ8","toVuePageData",[47,131,133],{"class":132},"shFtX"," =",[47,135,136],{"class":132}," (",[47,138,140],{"class":139},"s4oTP","j",[47,142,143],{"class":132},": ",[47,145,147],{"class":146},"sSkh3","JournalJson",[47,149,150],{"class":132},"):",[47,152,153],{"class":146}," VuePageData",[47,155,156],{"class":132}," =>",[47,158,159],{"class":132}," ({\n",[47,161,163,167,169,172,175,177],{"class":113,"line":162},3,[47,164,166],{"class":165},"sz8Xr","  meta",[47,168,143],{"class":132},[47,170,171],{"class":128},"pickMeta",[47,173,174],{"class":132},"(",[47,176,140],{"class":139},[47,178,179],{"class":132},"),\n",[47,181,183,186,188,191,193,195],{"class":113,"line":182},4,[47,184,185],{"class":165},"  prepostedEntries",[47,187,143],{"class":132},[47,189,190],{"class":128},"extractPreposted",[47,192,174],{"class":132},[47,194,140],{"class":139},[47,196,179],{"class":132},[47,198,200,203,205,208,210,212],{"class":113,"line":199},5,[47,201,202],{"class":165},"  currentEntries",[47,204,143],{"class":132},[47,206,207],{"class":128},"extractCurrent",[47,209,174],{"class":132},[47,211,140],{"class":139},[47,213,179],{"class":132},[47,215,217,220,222,225,227,229,232],{"class":113,"line":216},6,[47,218,219],{"class":165},"  netAssetsKeys",[47,221,143],{"class":132},[47,223,224],{"class":128},"collectNetAssetsKeys",[47,226,174],{"class":132},[47,228,140],{"class":139},[47,230,231],{"class":132},"), ",[47,233,234],{"class":117},"// re 集約のため\n",[47,236,238],{"class":113,"line":237},7,[47,239,240],{"class":132},"})\n",[14,242,243],{"id":243},"レイアウト調整で時間を吸われた",[10,245,246,247,250],{},"機械的な移植が終わったあと、PC ワイド画面で見たときの息苦しさが気になった。仕訳カードがタブで重なって表示されているせいで、画面の右半分が広いのに情報密度が上がらない。",[24,248,249],{},"isWide"," を判定して、ワイドなら縦並び、狭いならタブ形式、という分岐を入れた。",[10,252,253,254,257,258,261,262,265,266,268],{},"ここでハイドレーション mismatch を踏んだ。タブ切替も仕訳帳トグルも反応しない。原因は SSR 側の初期値を「ワイド」前提で書いたのに、クライアント側の最初のレンダリング時点では ",[24,255,256],{},"window"," が未定義で ",[24,259,260],{},"false"," 評価になっていたこと。コンソールに mismatch 警告が出ていた。SSR 初期値を「狭い画面前提」に統一して、",[24,263,264],{},"onMounted"," 後に ",[24,267,249],{}," を切り替える形に直したら、両環境ともクリック反応が戻った。",[14,270,272],{"id":271},"resizeobserver-で-bspl-を仕訳帳に追従させる","ResizeObserver で BS/PL を仕訳帳に追従させる",[10,274,275],{},"ワイドレイアウトで仕訳帳の開閉ボタンを押すと、左カラム（取引・仕訳候補）が一緒に伸縮してしまう違和感が出た。BS/PL パネルの高さに左カラムを合わせたいだけで、仕訳帳の開閉に巻き込まれたくない。ResizeObserver で BS/PL パネルの高さを観察して、CSS 変数として親に渡す方式に切り替えた。",[103,277,279],{"className":105,"code":278,"language":107,"meta":108,"style":108},"const observer = new ResizeObserver((entries) => {\n  // contentRect.height は padding を含まない\n  const h = (entries[0].target as HTMLElement).offsetHeight\n  el.style.setProperty('--bs-pl-height', `${h}px`)\n})\n",[24,280,281,309,314,353,404],{"__ignoreMap":108},[47,282,283,285,288,290,293,296,299,301,304,306],{"class":113,"line":114},[47,284,125],{"class":124},[47,286,287],{"class":139},"observer",[47,289,133],{"class":132},[47,291,292],{"class":124}," new ",[47,294,295],{"class":128},"ResizeObserver",[47,297,298],{"class":132},"((",[47,300,38],{"class":139},[47,302,303],{"class":132},")",[47,305,156],{"class":132},[47,307,308],{"class":132}," {\n",[47,310,311],{"class":113,"line":121},[47,312,313],{"class":117},"  // contentRect.height は padding を含まない\n",[47,315,316,319,322,324,326,328,331,334,337,340,344,347,350],{"class":113,"line":162},[47,317,318],{"class":124},"  const ",[47,320,321],{"class":139},"h",[47,323,133],{"class":132},[47,325,136],{"class":132},[47,327,38],{"class":139},[47,329,330],{"class":132},"[",[47,332,49],{"class":333},"sM54T",[47,335,336],{"class":132},"].",[47,338,339],{"class":139},"target",[47,341,343],{"class":342},"sHkkW"," as",[47,345,346],{"class":146}," HTMLElement",[47,348,349],{"class":132},").",[47,351,352],{"class":139},"offsetHeight\n",[47,354,355,358,361,364,366,369,371,375,379,381,384,387,390,392,395,398,401],{"class":113,"line":182},[47,356,357],{"class":139},"  el",[47,359,360],{"class":132},".",[47,362,363],{"class":139},"style",[47,365,360],{"class":132},[47,367,368],{"class":128},"setProperty",[47,370,174],{"class":132},[47,372,374],{"class":373},"sMJiu","'",[47,376,378],{"class":377},"sdGka","--bs-pl-height",[47,380,374],{"class":373},[47,382,383],{"class":132},",",[47,385,386],{"class":373}," `",[47,388,389],{"class":342},"${",[47,391,321],{"class":377},[47,393,394],{"class":342},"}",[47,396,397],{"class":377},"px",[47,399,400],{"class":373},"`",[47,402,403],{"class":132},")\n",[47,405,406],{"class":113,"line":199},[47,407,240],{"class":132},[10,409,410,411,414,415,418,419,421,422,425],{},"最初 ",[24,412,413],{},"entry.contentRect.height"," で取っていたら初期値だけ数pxズレた。初期値は ",[24,416,417],{},"offsetHeight","（padding 含む）で計算していたので、ResizeObserver 側も ",[24,420,417],{}," を読み直して揃えた。",[24,423,424],{},"align-items: start"," を付けて左右カラムが上端で揃うようにしたところで、ようやく仕訳帳を開閉しても左カラムが微動だにしなくなった。",[10,427,428],{},"ついでにもう1個踏んだ。仕訳帳を開いた瞬間に BS/PL の数値だけが消える現象。原因は左カラム側の上下に白背景が乗っていて、ワイド時に重なり合う領域で BS/PL を覆い隠していたこと。透明にして決着。",[14,430,431],{"id":431},"学びメモ",[82,433,434,437,440,453],{},[85,435,436],{},"79 件を一括変換する前に、最初の1件で 500 エラーを2つ踏めたから、残り 78 件で同じ穴に落ちずに済んだ。プロトタイプは「動くもの」を作る作業ではなく、「壊れるもの」を作る作業だった",[85,438,439],{},"ハイドレーション mismatch は、SSR 初期値とクライアント初期値の食い違いを読めば必ず再現する。今回は初期値を「狭い画面前提」に倒して mount 後に切り替える方針で揃えた",[85,441,442,443,446,447,449,450,452],{},"ResizeObserver の ",[24,444,445],{},"contentRect.height"," は padding を含まない。初期値で ",[24,448,417],{}," を使うなら ResizeObserver 側も ",[24,451,417],{}," を読まないと数pxズレる",[85,454,455],{},"機械的な変換が終わってからのレイアウト調整に予想の3倍時間を吸われた。一括移植のフェーズはコード生成より UI 仕上げのほうが長い",[14,457,458],{"id":458},"試行錯誤の記録",[82,460,461,464,467,470],{},[85,462,463],{},"1FY 構成にしたら表示が通った → 2FY 構成（前期 preposted）に戻して再現確認 → コンポーネントを null-safe 化",[85,465,466],{},"Nuxt が新規ファイルを認識しない → dev server を再起動",[85,468,469],{},"タブが反応しない → コンソールに mismatch 警告 → SSR 初期値を狭い画面前提に修正",[85,471,472],{},"BS/PL の数値が消える → 左カラムの白背景を透明化",[14,474,475],{"id":475},"明日やること",[82,477,480,493,499],{"className":478},[479],"contains-task-list",[85,481,484,489,490,492],{"className":482},[483],"task-list-item",[485,486],"input",{"disabled":487,"type":488},true,"checkbox"," 79 論点のうち、",[24,491,96],{}," セクションを含む複雑論点をいくつかピックアップして、scenario の出し分けが意図どおりかブラウザで再確認する",[85,494,496,498],{"className":495},[483],[485,497],{"disabled":487,"type":488}," インデックスページの章グルーピングが多すぎて縦に長いので、章ごとに折りたたみを入れる",[85,500,502,504],{"className":501},[483],[485,503],{"disabled":487,"type":488}," 手書き 6 論点と新規 79 論点でカードのスタイルが微妙に違うので、共通コンポーネントに寄せる",[363,506,507],{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .sSkh3, html code.shiki .sSkh3{--shiki-default:#2E8F82;--shiki-dark:#2E8F82}html pre.shiki code .sz8Xr, html code.shiki .sz8Xr{--shiki-default:#998418;--shiki-dark:#998418}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}",{"title":108,"searchDepth":121,"depth":121,"links":509},[510,511,512,513,514,515,516],{"id":16,"depth":121,"text":16},{"id":68,"depth":121,"text":69},{"id":243,"depth":121,"text":243},{"id":271,"depth":121,"text":272},{"id":431,"depth":121,"text":431},{"id":458,"depth":121,"text":458},{"id":475,"depth":121,"text":475},"dev","Excel→TypeScript パイプラインで生成した79論点を Vueページ として一括出力。PCワイド画面では仕訳カード縦並び、狭い画面ではタブ形式に切り替えるレスポンシブUIも実装","md",{},"/2026-05-03-case100-vue-migration","eurekapu-nuxt4",false,"2026-05-03T00:00:00.000Z",{"title":5,"description":518},"2026-05/2026-05-03/case100-vue-migration",[528,529,530,531,295],"case100","Vue","Nuxt","ハイドレーション",null,"J1ASBCHF0FDe0wCPqnh4vNp356MTa-yxjByyDs4aJLc",[],"https://log.eurekapu.com/favicon.svg",1778379990187]