[{"data":1,"prerenderedAt":633},["ShallowReactive",2],{"content-/contribution-heatmap-and-krw-label-fix":3,"all-pages-for-dir":612,"related-/contribution-heatmap-and-krw-label-fix":613,"og-image-/contribution-heatmap-and-krw-label-fix":632},{"id":4,"title":5,"body":6,"category":592,"concepts":593,"description":594,"extension":595,"meta":596,"navigation":597,"ogImage":593,"path":598,"project_name":599,"published":600,"publishedAt":601,"seo":602,"source":603,"stem":604,"tags":605,"todo":593,"unpublished":600,"updatedAt":593,"__hash__":611},"pages/2026-07/2026-07-13/contribution-heatmap-and-krw-label-fix.md","トップページにGitHub草風ヒートマップを実装、テスト18,440件全パス化とKRW単位ラベルの根本修正",{"type":7,"value":8,"toc":586},"minimark",[9,14,18,22,25,28,56,59,63,66,73,81,84,172,179,183,186,189,192,231,532,535,562,565,568,582],[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],{},"切り分けの途中で見つかったのが、earningsDynamics の KRW 銘柄（韓国ウォン建て）の問題。売上が兆ウォンオーダーの銘柄のチャートに、$B（ドルビリオン）のラベルがぶら下がっていた。83兆ウォンが「$83.3B」として表示される。桁も通貨も嘘になる。",[15,187,188],{},"これはテストの期待値を書き換えて済ませる話ではない。「コリアンウォンのドルビリオンラベル問題は、ちゃんと直してほしい。テストで全部担保できるようにしてほしい」と指示して、生成パイプラインから直させた。",[15,190,191],{},"修正の構成:",[29,193,194,212,225],{},[32,195,196,199,200,203,204,207,208,211],{},[77,197,198],{},"生成側",": ",[35,201,202],{},"buildTickerDataFromTurso"," と生成スクリプト ",[35,205,206],{},"generate-earnings-dynamics-data.mjs"," の両方に、ティッカーごとの通貨単位テーブル（",[35,209,210],{},"KRW_TICKER_UNITS","）を追加。KRW 銘柄の売上は百万ウォン→兆ウォンに換算し、EPS はウォンのまま持つ",[32,213,214,199,217,220,221,224],{},[77,215,216],{},"描画側",[35,218,219],{},"EarningsChart.vue"," に埋まっていた数値整形を ",[35,222,223],{},"app/utils/earningsDynamics/format.ts"," の純粋関数に切り出し、単位に応じて整形する",[32,226,227,230],{},[77,228,229],{},"目盛りの桁合わせ",": ウォン建て EPS は数万オーダーになるため、チャート目盛りの丸め単位も KRW 銘柄では桁を合わせて別に持つ",[232,233,238],"pre",{"className":234,"code":235,"language":236,"meta":237,"style":237},"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,239,240,297,312,371,409,447,485,520,526],{"__ignoreMap":237},[241,242,245,249,253,257,261,264,268,270,274,277,280,282,285,288,291,294],"span",{"class":243,"line":244},"line",1,[241,246,248],{"class":247},"sHkkW","export",[241,250,252],{"class":251},"stQ0i"," const ",[241,254,256],{"class":255},"senZ8","formatMetricValue",[241,258,260],{"class":259},"shFtX"," =",[241,262,263],{"class":259}," (",[241,265,267],{"class":266},"s4oTP","unit",[241,269,199],{"class":259},[241,271,273],{"class":272},"sSkh3","MetricUnit",[241,275,276],{"class":259},",",[241,278,279],{"class":266}," v",[241,281,199],{"class":259},[241,283,284],{"class":272},"number",[241,286,287],{"class":259},"):",[241,289,290],{"class":272}," string",[241,292,293],{"class":259}," =>",[241,295,296],{"class":259}," {\n",[241,298,300,303,305,307,310],{"class":243,"line":299},2,[241,301,302],{"class":247},"  switch",[241,304,263],{"class":259},[241,306,267],{"class":266},[241,308,309],{"class":259},")",[241,311,296],{"class":259},[241,313,315,318,322,326,329,332,335,338,341,344,347,350,353,356,360,362,365,368],{"class":243,"line":314},3,[241,316,317],{"class":247},"    case",[241,319,321],{"class":320},"sMJiu"," '",[241,323,325],{"class":324},"sdGka","$B",[241,327,328],{"class":320},"'",[241,330,331],{"class":259},":",[241,333,334],{"class":247}," return",[241,336,337],{"class":320}," `",[241,339,340],{"class":324},"$",[241,342,343],{"class":247},"${",[241,345,346],{"class":324},"v",[241,348,349],{"class":259},".",[241,351,352],{"class":255},"toFixed",[241,354,355],{"class":259},"(",[241,357,359],{"class":358},"sM54T","1",[241,361,309],{"class":259},[241,363,364],{"class":247},"}",[241,366,367],{"class":324},"B",[241,369,370],{"class":320},"`\n",[241,372,374,376,378,380,382,384,386,388,390,392,394,396,398,400,403,405,407],{"class":243,"line":373},4,[241,375,317],{"class":247},[241,377,321],{"class":320},[241,379,340],{"class":324},[241,381,328],{"class":320},[241,383,331],{"class":259},[241,385,334],{"class":247},[241,387,337],{"class":320},[241,389,340],{"class":324},[241,391,343],{"class":247},[241,393,346],{"class":324},[241,395,349],{"class":259},[241,397,352],{"class":255},[241,399,355],{"class":259},[241,401,402],{"class":358},"2",[241,404,309],{"class":259},[241,406,364],{"class":247},[241,408,370],{"class":320},[241,410,412,414,416,419,421,423,425,427,429,431,433,435,437,439,441,443,445],{"class":243,"line":411},5,[241,413,317],{"class":247},[241,415,321],{"class":320},[241,417,418],{"class":324},"兆W",[241,420,328],{"class":320},[241,422,331],{"class":259},[241,424,334],{"class":247},[241,426,337],{"class":320},[241,428,343],{"class":247},[241,430,346],{"class":324},[241,432,349],{"class":259},[241,434,352],{"class":255},[241,436,355],{"class":259},[241,438,402],{"class":358},[241,440,309],{"class":259},[241,442,364],{"class":247},[241,444,418],{"class":324},[241,446,370],{"class":320},[241,448,450,452,454,457,459,461,463,465,467,469,471,473,475,477,479,481,483],{"class":243,"line":449},6,[241,451,317],{"class":247},[241,453,321],{"class":320},[241,455,456],{"class":324},"兆ウォン",[241,458,328],{"class":320},[241,460,331],{"class":259},[241,462,334],{"class":247},[241,464,337],{"class":320},[241,466,343],{"class":247},[241,468,346],{"class":324},[241,470,349],{"class":259},[241,472,352],{"class":255},[241,474,355],{"class":259},[241,476,359],{"class":358},[241,478,309],{"class":259},[241,480,364],{"class":247},[241,482,456],{"class":324},[241,484,370],{"class":320},[241,486,488,490,492,495,497,499,501,503,505,508,510,512,514,516,518],{"class":243,"line":487},7,[241,489,317],{"class":247},[241,491,321],{"class":320},[241,493,494],{"class":324},"ウォン",[241,496,328],{"class":320},[241,498,331],{"class":259},[241,500,334],{"class":247},[241,502,337],{"class":320},[241,504,343],{"class":247},[241,506,507],{"class":255},"fmtKrwInteger",[241,509,355],{"class":259},[241,511,346],{"class":324},[241,513,309],{"class":259},[241,515,364],{"class":247},[241,517,494],{"class":324},[241,519,370],{"class":320},[241,521,523],{"class":243,"line":522},8,[241,524,525],{"class":259},"  }\n",[241,527,529],{"class":243,"line":528},9,[241,530,531],{"class":259},"}\n",[15,533,534],{},"テストは3層で担保させた。",[536,537,538,547,556],"ol",{},[32,539,540,199,543,546],{},[77,541,542],{},"生成",[35,544,545],{},"buildTickerData.test.ts"," に KRW 銘柄のケースを追加。Turso の生データから正しい単位でデータが組み上がるか",[32,548,549,199,552,555],{},[77,550,551],{},"整形",[35,553,554],{},"earnings-dynamics-format.test.ts"," で「KRW 銘柄に $ ラベルを出してはならない」を直接検証",[32,557,558,561],{},[77,559,560],{},"仕様固定",": USD 銘柄の従来出力が1文字も変わっていないことを固定（修正の巻き添えで既存表示が壊れるのを防ぐ）",[15,563,564],{},"データを再生成して全銘柄を更新し、全体スイートで回帰確認。全18,506件がパスして、カバレッジも取得した。生成スクリプト側にも同じ KRW 対応を入れたのは、TS 側だけ直すと次のデータ再生成で同じバグが復活するから。訂正は1箇所で終わらせず、同じロジックを持つ場所すべてに展開する。",[19,566,567],{"id":567},"学び",[29,569,570,573,576,579],{},[32,571,572],{},"テスト失敗の一括修正は「全部テストを直す」に流れやすい。1件ずつ「テストが古い / コードのバグ」を判定させると、34件のなかから本物のデータバグ（KRW ラベル）が1つ出てきた",[32,574,575],{},"双子実装（TS の純粋関数と生成スクリプトの inline 実装）があるときは、片方だけ直しても再生成で巻き戻る。両方直して、Vitest 側で仕様を凍結する",[32,577,578],{},"「既存出力を変えない」こと自体をテストに書いておくと、単位対応のような横断修正でも安心して入れられる",[32,580,581],{},"チャートの数値整形をコンポーネントから純粋関数に切り出したことで、ブラウザを開かずに表示仕様をテストできるようになった",[583,584,585],"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":237,"searchDepth":299,"depth":299,"links":587},[588,589,590,591],{"id":21,"depth":299,"text":21},{"id":61,"depth":299,"text":62},{"id":181,"depth":299,"text":182},{"id":567,"depth":299,"text":567},"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":594},"make-diary","2026-07/2026-07-13/contribution-heatmap-and-krw-label-fix",[606,607,608,609,610],"Nuxt","Vitest","テスト","データ可視化","earnings-dynamics","RbqmnT4zl0fKiKeWVht68yXYy1mihvnXnAHQrRzdOzI",[],[614,617,620,624,628],{"title":615,"path":616,"publishedAt":601},"2026年7月13日の開発日記 - 行きたい国リストの費用シミュレーションとテスト34件の切り分け","/2026-07-13-diary",{"title":618,"path":619,"publishedAt":601},"月次統計カードにNEWバッジを実装 - データ鮮度で自動点灯・自動消灯させる","/memory-makers-new-badge",{"title":621,"path":622,"publishedAt":623},"韓国半導体輸出ページに速報値チャートを拡充した - DRAM/NAND単価・SSD/MCPをPDF速報から反映","/korea-chip-export-flash-charts","2026-07-04T00:00:00.000Z",{"title":625,"path":626,"publishedAt":627},"Micron Q3 FY26 決算をbeat-monitoringとearnings-dynamicsに手作業で反映した記録","/micron-q3-fy26-beat-monitoring-coverage","2026-06-25T00:00:00.000Z",{"title":629,"path":630,"publishedAt":631},"メモリーメーカー動向ハブを新設し、DRAM市場シェアの推移を可視化した一日","/memory-makers-hub-dram-share","2026-06-02T00:00:00.000Z","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=d033fe6ebaddc117",1785654983947]