[{"data":1,"prerenderedAt":620},["ShallowReactive",2],{"content-/contribution-heatmap-and-krw-label-fix":3,"all-pages-for-dir":618,"og-image-/contribution-heatmap-and-krw-label-fix":619},{"id":4,"title":5,"body":6,"category":599,"concepts":600,"description":601,"extension":602,"meta":603,"navigation":604,"ogImage":600,"path":605,"project_name":606,"published":607,"publishedAt":608,"seo":609,"stem":610,"tags":611,"todo":600,"unpublished":607,"updatedAt":600,"__hash__":617},"pages/2026-07/2026-07-13/contribution-heatmap-and-krw-label-fix.md","トップページにGitHub草風ヒートマップを実装、テスト18,440件全パス化とKRW単位ラベルの根本修正",{"type":7,"value":8,"toc":593},"minimark",[9,14,18,22,25,28,56,59,63,66,73,81,84,172,179,183,193,196,199,238,539,542,569,572,575,589],[10,11,13],"h1",{"id":12},"トップページに草を生やしテスト失敗34件を切り分けてkrwラベルを根本修正した","トップページに草を生やし、テスト失敗34件を切り分けてKRWラベルを根本修正した",[15,16,17],"p",{},"トップページを開くたびに「Nuxt Content を使ったメモ公開サイトです」という説明文が目に入る。見りゃわかる。この文章を消して、代わりに GitHub のコントリビューショングラフ、いわゆる「草」を生やしてもらうことにした。ついでに放置していたユニットテストの失敗34件を全部片付けたら、そのなかから「韓国ウォン建て銘柄の値に $B ラベルが付いている」というデータバグが転がり出てきた。この1件だけはテストを直すのではなく、パイプラインの根本から直させた。",[19,20,21],"h2",{"id":21},"トップページに草を生やす",[15,23,24],{},"Claude Code への指示はほぼ口頭のノリのまま。「説明文を消して、その位置に GitHub の草みたいなやつを横幅いっぱいで」。",[15,26,27],{},"実装させた内容:",[29,30,31,39,46,49],"ul",{},[32,33,34,38],"li",{},[35,36,37],"code",{},"ContributionGraph.vue"," を新規作成。直近1年・53週分のグリッドに、公開記事1,474本の投稿日を緑の濃淡で表示する",[32,40,41,42,45],{},"トップページの説明文と、不要になった ",[35,43,44],{},".intro p"," のスタイルを削除",[32,47,48],{},"記事データの取得は軽量版の composable に切り替え（全フィールドを引かず、ヒートマップに必要な日付だけ取る）",[32,50,51,52,55],{},"ユニットテスト（",[35,53,54],{},"tests/contribution-graph.test.ts","）を追加",[15,57,58],{},"最後にブラウザで描画とコンソールを確認させて完了。トップページの一等地に、投稿が続いた週は濃い緑、サボった週は薄いマスが並ぶようになった。自分のブログなのに、他人のGitHubプロフィールを覗くときと同じ目線でマスの濃さを数えてしまう。",[19,60,62],{"id":61},"テスト失敗34件をテストが古いのかコードのバグなのかで切り分ける","テスト失敗34件を「テストが古いのか、コードのバグなのか」で切り分ける",[15,64,65],{},"草の実装でテストを回したら、既存のユニットテストが34件落ちていた。「パスしてほしいんで、失敗34件、正しく修正してもらえませんか」と投げて、1件ずつ切り分けさせた。",[15,67,68,69,72],{},"進め方はまず全失敗の詳細を1つのファイルに落とし、クラスタごとに原因を特定してから修正に入る流れ。途中、design-principles 配下のファイル名をグロブで空振りしていないか確認させたり、",[35,70,71],{},"tsukumi-slides.vue"," のキーボードハンドラの挙動を実際に追わせたりと、「テストが間違っている」と断定する前に実装側の裏を取っている。",[15,74,75,76,80],{},"ここで重要なのは、34件を機械的に「テストを直して緑にする」のではなく、",[77,78,79],"strong",{},"1件ごとに「テストが古いのか、データ・コードのバグなのか」を判定する","こと。全部テスト側を直せば緑にはなるが、それはバグを仕様として固定する行為になる。",[15,82,83],{},"切り分けの結果:",[85,86,87,106],"table",{},[88,89,90],"thead",{},[91,92,93,97,100,103],"tr",{},[94,95,96],"th",{},"クラスタ",[94,98,99],{},"件数",[94,101,102],{},"判定",[94,104,105],{},"対処",[107,108,109,128,142,156],"tbody",{},[91,110,111,115,118,121],{},[112,113,114],"td",{},"triple-beat 系",[112,116,117],{},"18件",[112,119,120],{},"テストが古い",[112,122,123,124,127],{},"データに追加された「次回決算の予測行」（",[35,125,126],{},"isForecast: true","）を実績とみなさない規約に合わせてテスト側を更新",[91,129,130,133,136,139],{},[112,131,132],{},"OGタグ不足",[112,134,135],{},"5ページ分",[112,137,138],{},"ページ側の不備",[112,140,141],{},"公開5ページにOGタグを追加",[91,143,144,147,150,153],{},[112,145,146],{},"url-migration",[112,148,149],{},"数件",[112,151,152],{},"テストの前提漏れ",[112,154,155],{},"standalone コンテンツをテストの対象から除外",[91,157,158,161,164,169],{},[112,159,160],{},"KRW銘柄の単位ラベル",[112,162,163],{},"—",[112,165,166],{},[77,167,168],{},"データ・パイプラインのバグ",[112,170,171],{},"テストは触らず根本修正へ（次節）",[15,173,174,175,178],{},"triple-beat 系の18件は、先例となる ",[35,176,177],{},"detect-new-earnings.test.ts"," の規約（予測行は実績として数えない）に揃える形でテストを更新させた。ここまでで146ファイル・18,440件が全パス。",[19,180,182],{"id":181},"krwの-b-ラベルだけはテストを直さない","KRWの $B ラベルだけはテストを直さない",[15,184,185,186,192],{},"切り分けの途中で見つかったのが、earningsDynamics の KRW 銘柄（韓国ウォン建て）の問題。売上が兆ウォンオーダーの銘柄のチャートに、",[35,187,191],{"className":188},[189,190],"language-math","math-inline","B（ドルビリオン）のラベルがぶら下がっていた。83兆ウォンが「","83.3B」として表示される。桁も通貨も嘘になる。",[15,194,195],{},"これはテストの期待値を書き換えて済ませる話ではない。「コリアンウォンのドルビリオンラベル問題は、ちゃんと直してほしい。テストで全部担保できるようにしてほしい」と指示して、生成パイプラインから直させた。",[15,197,198],{},"修正の構成:",[29,200,201,219,232],{},[32,202,203,206,207,210,211,214,215,218],{},[77,204,205],{},"生成側",": ",[35,208,209],{},"buildTickerDataFromTurso"," と生成スクリプト ",[35,212,213],{},"generate-earnings-dynamics-data.mjs"," の両方に、ティッカーごとの通貨単位テーブル（",[35,216,217],{},"KRW_TICKER_UNITS","）を追加。KRW 銘柄の売上は百万ウォン→兆ウォンに換算し、EPS はウォンのまま持つ",[32,220,221,206,224,227,228,231],{},[77,222,223],{},"描画側",[35,225,226],{},"EarningsChart.vue"," に埋まっていた数値整形を ",[35,229,230],{},"app/utils/earningsDynamics/format.ts"," の純粋関数に切り出し、単位に応じて整形する",[32,233,234,237],{},[77,235,236],{},"目盛りの桁合わせ",": ウォン建て EPS は数万オーダーになるため、チャート目盛りの丸め単位も KRW 銘柄では桁を合わせて別に持つ",[239,240,245],"pre",{"className":241,"code":242,"language":243,"meta":244,"style":244},"language-typescript shiki shiki-themes vitesse-light vitesse-light","export const formatMetricValue = (unit: MetricUnit, v: number): string => {\n  switch (unit) {\n    case '$B': return `$${v.toFixed(1)}B`\n    case '$': return `$${v.toFixed(2)}`\n    case '兆W': return `${v.toFixed(2)}兆W`\n    case '兆ウォン': return `${v.toFixed(1)}兆ウォン`\n    case 'ウォン': return `${fmtKrwInteger(v)}ウォン`\n  }\n}\n","typescript","",[35,246,247,304,319,378,416,454,492,527,533],{"__ignoreMap":244},[248,249,252,256,260,264,268,271,275,277,281,284,287,289,292,295,298,301],"span",{"class":250,"line":251},"line",1,[248,253,255],{"class":254},"sHkkW","export",[248,257,259],{"class":258},"stQ0i"," const ",[248,261,263],{"class":262},"senZ8","formatMetricValue",[248,265,267],{"class":266},"shFtX"," =",[248,269,270],{"class":266}," (",[248,272,274],{"class":273},"s4oTP","unit",[248,276,206],{"class":266},[248,278,280],{"class":279},"sSkh3","MetricUnit",[248,282,283],{"class":266},",",[248,285,286],{"class":273}," v",[248,288,206],{"class":266},[248,290,291],{"class":279},"number",[248,293,294],{"class":266},"):",[248,296,297],{"class":279}," string",[248,299,300],{"class":266}," =>",[248,302,303],{"class":266}," {\n",[248,305,307,310,312,314,317],{"class":250,"line":306},2,[248,308,309],{"class":254},"  switch",[248,311,270],{"class":266},[248,313,274],{"class":273},[248,315,316],{"class":266},")",[248,318,303],{"class":266},[248,320,322,325,329,333,336,339,342,345,348,351,354,357,360,363,367,369,372,375],{"class":250,"line":321},3,[248,323,324],{"class":254},"    case",[248,326,328],{"class":327},"sMJiu"," '",[248,330,332],{"class":331},"sdGka","$B",[248,334,335],{"class":327},"'",[248,337,338],{"class":266},":",[248,340,341],{"class":254}," return",[248,343,344],{"class":327}," `",[248,346,347],{"class":331},"$",[248,349,350],{"class":254},"${",[248,352,353],{"class":331},"v",[248,355,356],{"class":266},".",[248,358,359],{"class":262},"toFixed",[248,361,362],{"class":266},"(",[248,364,366],{"class":365},"sM54T","1",[248,368,316],{"class":266},[248,370,371],{"class":254},"}",[248,373,374],{"class":331},"B",[248,376,377],{"class":327},"`\n",[248,379,381,383,385,387,389,391,393,395,397,399,401,403,405,407,410,412,414],{"class":250,"line":380},4,[248,382,324],{"class":254},[248,384,328],{"class":327},[248,386,347],{"class":331},[248,388,335],{"class":327},[248,390,338],{"class":266},[248,392,341],{"class":254},[248,394,344],{"class":327},[248,396,347],{"class":331},[248,398,350],{"class":254},[248,400,353],{"class":331},[248,402,356],{"class":266},[248,404,359],{"class":262},[248,406,362],{"class":266},[248,408,409],{"class":365},"2",[248,411,316],{"class":266},[248,413,371],{"class":254},[248,415,377],{"class":327},[248,417,419,421,423,426,428,430,432,434,436,438,440,442,444,446,448,450,452],{"class":250,"line":418},5,[248,420,324],{"class":254},[248,422,328],{"class":327},[248,424,425],{"class":331},"兆W",[248,427,335],{"class":327},[248,429,338],{"class":266},[248,431,341],{"class":254},[248,433,344],{"class":327},[248,435,350],{"class":254},[248,437,353],{"class":331},[248,439,356],{"class":266},[248,441,359],{"class":262},[248,443,362],{"class":266},[248,445,409],{"class":365},[248,447,316],{"class":266},[248,449,371],{"class":254},[248,451,425],{"class":331},[248,453,377],{"class":327},[248,455,457,459,461,464,466,468,470,472,474,476,478,480,482,484,486,488,490],{"class":250,"line":456},6,[248,458,324],{"class":254},[248,460,328],{"class":327},[248,462,463],{"class":331},"兆ウォン",[248,465,335],{"class":327},[248,467,338],{"class":266},[248,469,341],{"class":254},[248,471,344],{"class":327},[248,473,350],{"class":254},[248,475,353],{"class":331},[248,477,356],{"class":266},[248,479,359],{"class":262},[248,481,362],{"class":266},[248,483,366],{"class":365},[248,485,316],{"class":266},[248,487,371],{"class":254},[248,489,463],{"class":331},[248,491,377],{"class":327},[248,493,495,497,499,502,504,506,508,510,512,515,517,519,521,523,525],{"class":250,"line":494},7,[248,496,324],{"class":254},[248,498,328],{"class":327},[248,500,501],{"class":331},"ウォン",[248,503,335],{"class":327},[248,505,338],{"class":266},[248,507,341],{"class":254},[248,509,344],{"class":327},[248,511,350],{"class":254},[248,513,514],{"class":262},"fmtKrwInteger",[248,516,362],{"class":266},[248,518,353],{"class":331},[248,520,316],{"class":266},[248,522,371],{"class":254},[248,524,501],{"class":331},[248,526,377],{"class":327},[248,528,530],{"class":250,"line":529},8,[248,531,532],{"class":266},"  }\n",[248,534,536],{"class":250,"line":535},9,[248,537,538],{"class":266},"}\n",[15,540,541],{},"テストは3層で担保させた。",[543,544,545,554,563],"ol",{},[32,546,547,206,550,553],{},[77,548,549],{},"生成",[35,551,552],{},"buildTickerData.test.ts"," に KRW 銘柄のケースを追加。Turso の生データから正しい単位でデータが組み上がるか",[32,555,556,206,559,562],{},[77,557,558],{},"整形",[35,560,561],{},"earnings-dynamics-format.test.ts"," で「KRW 銘柄に $ ラベルを出してはならない」を直接検証",[32,564,565,568],{},[77,566,567],{},"仕様固定",": USD 銘柄の従来出力が1文字も変わっていないことを固定（修正の巻き添えで既存表示が壊れるのを防ぐ）",[15,570,571],{},"データを再生成して全銘柄を更新し、全体スイートで回帰確認。全18,506件がパスして、カバレッジも取得した。生成スクリプト側にも同じ KRW 対応を入れたのは、TS 側だけ直すと次のデータ再生成で同じバグが復活するから。訂正は1箇所で終わらせず、同じロジックを持つ場所すべてに展開する。",[19,573,574],{"id":574},"学び",[29,576,577,580,583,586],{},[32,578,579],{},"テスト失敗の一括修正は「全部テストを直す」に流れやすい。1件ずつ「テストが古い / コードのバグ」を判定させると、34件のなかから本物のデータバグ（KRW ラベル）が1つ出てきた",[32,581,582],{},"双子実装（TS の純粋関数と生成スクリプトの inline 実装）があるときは、片方だけ直しても再生成で巻き戻る。両方直して、Vitest 側で仕様を凍結する",[32,584,585],{},"「既存出力を変えない」こと自体をテストに書いておくと、単位対応のような横断修正でも安心して入れられる",[32,587,588],{},"チャートの数値整形をコンポーネントから純粋関数に切り出したことで、ブラウザを開かずに表示仕様をテストできるようになった",[590,591,592],"style",{},"html pre.shiki code .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}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 .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}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}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);}",{"title":244,"searchDepth":306,"depth":306,"links":594},[595,596,597,598],{"id":21,"depth":306,"text":21},{"id":61,"depth":306,"text":62},{"id":181,"depth":306,"text":182},{"id":574,"depth":306,"text":574},"dev",null,"ブログトップにGitHubのコントリビューショングラフ風の投稿ヒートマップを実装。ユニットテストの失敗34件を1件ずつ切り分けて全パスにし、KRW銘柄に$Bラベルが付くバグだけは生成パイプラインから直した記録。","md",{},true,"/contribution-heatmap-and-krw-label-fix","mdx-playground",false,"2026-07-13T00:00:00.000Z",{"title":5,"description":601},"2026-07/2026-07-13/contribution-heatmap-and-krw-label-fix",[612,613,614,615,616],"Nuxt","Vitest","テスト","データ可視化","earnings-dynamics","hgu-vhMUWvX_qjwd-j7pxq_O2mcK9QX7wFmyokJS-gs",[],"https://log.eurekapu.com/og/blog/contribution-heatmap-and-krw-label-fix.png?v=2026-07-13T00%3A00%3A00.000Z&title=%E3%83%88%E3%83%83%E3%83%97%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%ABGitHub%E8%8D%89%E9%A2%A8%E3%83%92%E3%83%BC%E3%83%88%E3%83%9E%E3%83%83%E3%83%97%E3%82%92%E5%AE%9F%E8%A3%85%E3%80%81%E3%83%86%E3%82%B9%E3%83%8818%2C440%E4%BB%B6%E5%85%A8%E3%83%91%E3%82%B9%E5%8C%96%E3%81%A8KRW%E5%8D%98%E4%BD%8D%E3%83%A9%E3%83%99%E3%83%AB%E3%81%AE%E6%A0%B9%E6%9C%AC%E4%BF%AE%E6%AD%A3&author=Kei%20Komatsu&sig=6320f1e9eb34291a",1784001814777]