[{"data":1,"prerenderedAt":431},["ShallowReactive",2],{"content-/memory-makers-new-badge":3,"all-pages-for-dir":429,"og-image-/memory-makers-new-badge":430},{"id":4,"title":5,"body":6,"category":411,"concepts":412,"description":413,"extension":414,"meta":415,"navigation":184,"ogImage":412,"path":416,"project_name":417,"published":418,"publishedAt":419,"seo":420,"stem":421,"tags":422,"todo":412,"unpublished":418,"updatedAt":412,"__hash__":428},"pages/2026-07/2026-07-13/memory-makers-new-badge.md","月次統計カードにNEWバッジを実装 - データ鮮度で自動点灯・自動消灯させる",{"type":7,"value":8,"toc":403},"minimark",[9,13,18,21,50,53,57,60,63,66,70,73,85,99,103,106,109,119,150,155,366,369,372,376,379,382,385,399],[10,11,12],"p",{},"台湾旅行から戻った朝、5日分溜まった /make-diary の毎朝チェーンを再開した。7/8分の日記4本と統計チェーンを全部回し、最後は76ファイルを1コミットにまとめた。その途中で「既存データのバグでは」と疑って空振りした一幕と、月次統計ページに NEW バッジの自動減衰ロジックを実装させた本題を記録しておく。",[14,15,17],"h2",{"id":16},"_5日ぶりの統計チェーン再開","5日ぶりの統計チェーン再開",[10,19,20],{},"前回実行が7/8なので、決算・統計データの取得チェーンは5日分の空白を埋める形になった。回した結果:",[22,23,24,32,38,44],"ul",{},[25,26,27,31],"li",{},[28,29,30],"strong",{},"決算スナップショット",": 39銘柄すべて取得成功（ok=39 ng=0）。前日比の差分は5日ぶりなので変動が大きめに出た",[25,33,34,37],{},[28,35,36],{},"台湾ODM月次売上",": 2026年6月分が14銘柄で新規追加。TSデータ35件を再生成",[25,39,40,43],{},[28,41,42],{},"台湾総輸出",": 6月分を追記。品目内訳は情報通信が前年比+72.3%、電子部品が+32.8%と伸びが続いている",[25,45,46,49],{},[28,47,48],{},"earnings-beat-scan",": 直近24時間の決算から AZZ・LEVI・WDFC の3銘柄を抽出し、ガイダンスをコンセンサスと突合。AZZ は実績ビートのみで通年ガイダンスはほぼ横ばいだった",[10,51,52],{},"コミットは学習ゲート（/learn）のクイズに4問全問正解してから通した。76ファイル、+2998/-196行。",[14,54,56],{"id":55},"既存データのバグを疑って空振りした","「既存データのバグ」を疑って空振りした",[10,58,59],{},"台湾総輸出の6月分を追記する途中で、一度手が止まった。台湾MOFのプレスリリースPDFから抽出した値と突き合わせたところ、2025年6月の記録値が誤っているように見えたからだ。「記録済みの59.9は誤りで、正しくは53.32。既存データのバグだ」と一度は断定しかけた。",[10,61,62],{},"ところが前後月まで検証させると、exportStats.ts は最初から正しい値を持っていた。誤っていたのはデータではなく突き合わせ方で、自分の勘違いだった。疑い→検証→撤回まで数分で済んだが、「バグだ」と思った瞬間に修正へ走らず、周辺月まで機械的に照合させたのが効いた。ここで雑に「修正」していたら、正しいデータを壊すところだった。",[10,64,65],{},"データが正しく1ヶ月分増えた結果、テストのハードコード値（29ヶ月→30ヶ月）を更新する必要が出た。これはデータ追記に伴う正当な更新なので素直に直した。",[14,67,69],{"id":68},"見出しのをバグと誤診しかけた","見出しの「##」をバグと誤診しかけた",[10,71,72],{},"日記記事のQA中にも一件、誤診しかけた。詳細記事を抜き打ちで開くと、見出しの先頭に「##」が生のまま表示されている。Markdownのパース不良か、HMRの反映漏れか。リロードしても消えず、他の記事でも同じ表示が出る。",[10,74,75,76,80,81,84],{},"dev サーバーの再起動に進む前に、ブラウザのDOMを直接掘らせた。すると正体は ",[77,78,79],"code",{},"\u003Cspan class=\"heading-hash\" aria-hidden=\"true\">## \u003C/span>"," という明示的なDOM要素だった。DocPage.vue が見出しレベルに応じて ",[77,82,83],{},"#"," を挿入する、Markdownソース風の意図的な装飾。つまり自分のサイトのデザインを、自分でバグと誤診しかけていた。",[10,86,87,88,91,92,94,95,98],{},"再発防止として ",[77,89,90],{},".claude/rules/heading-hash-markers.md"," にルール化した。「見出しテキストの前に灰色の ",[77,93,83],{}," があり本文が黒なら仕様、",[77,96,97],{},"\u003Ch2>"," タグ自体が生成されず全体が1色のプレーンテキストなら本物のパース不良」という見分け方まで書いた。スクリーンショットの見た目ではなくDOMで判定する、が教訓。",[14,100,102],{"id":101},"本題-new-バッジの自動減衰","本題: NEW バッジの自動減衰",[10,104,105],{},"チェーンが終わったあと、月次統計ページ（/memory-makers/makers）を開いてカードを上から順に眺め、右上の年月表記を一枚ずつ見比べていた。今回どの銘柄が6月分に更新されたのか、いちいち探すのが面倒くさい。かといって「更新されました」の強調が7月中ずっと点きっぱなしなのも邪魔だ。",[10,107,108],{},"この要望をそのまま伝えて、実装させた。設計はこうなった:",[110,111,112],"ol",{},[25,113,114,115,118],{},"月次売上データの型に ",[77,116,117],{},"dataUpdatedAt"," を追加する。「値が最後に実際に変わった日時」で、Turso の updated_at 由来",[120,121,126],"pre",{"className":122,"code":123,"language":124,"meta":125,"style":125},"language-typescript shiki shiki-themes vitesse-light vitesse-light","/** データの値が最後に実際に変わった日時（ISO 8601 UTC）。NEW バッジの鮮度判定に使う */\ndataUpdatedAt?: string\n","typescript","",[77,127,128,137],{"__ignoreMap":125},[129,130,133],"span",{"class":131,"line":132},"line",1,[129,134,136],{"class":135},"sxvE3","/** データの値が最後に実際に変わった日時（ISO 8601 UTC）。NEW バッジの鮮度判定に使う */\n",[129,138,140,143,147],{"class":131,"line":139},2,[129,141,117],{"class":142},"s4oTP",[129,144,146],{"class":145},"stQ0i","?:",[129,148,149],{"class":142}," string\n",[110,151,152],{"start":139},[25,153,154],{},"鮮度判定は純粋関数に切り出す。更新から4日以内なら NEW を出し、期間が過ぎれば自動で消える",[120,156,158],{"className":122,"code":157,"language":124,"meta":125,"style":125},"export const FRESH_WINDOW_DAYS = 4\n\nexport function isFreshUpdate(dataUpdatedAt, now, windowDays = FRESH_WINDOW_DAYS) {\n  if (!dataUpdatedAt) return false\n  const t = Date.parse(dataUpdatedAt)\n  if (Number.isNaN(t)) return false\n  const diffMs = now.getTime() - t\n  return diffMs >= 0 && diffMs \u003C windowDays * DAY_MS  // 未来時刻（時計ズレ）は false\n}\n",[77,159,160,180,186,225,247,274,300,326,360],{"__ignoreMap":125},[129,161,162,166,169,172,176],{"class":131,"line":132},[129,163,165],{"class":164},"sHkkW","export",[129,167,168],{"class":145}," const ",[129,170,171],{"class":142},"FRESH_WINDOW_DAYS",[129,173,175],{"class":174},"shFtX"," =",[129,177,179],{"class":178},"sM54T"," 4\n",[129,181,182],{"class":131,"line":139},[129,183,185],{"emptyLinePlaceholder":184},true,"\n",[129,187,189,191,194,198,201,203,206,209,211,214,216,219,222],{"class":131,"line":188},3,[129,190,165],{"class":164},[129,192,193],{"class":145}," function",[129,195,197],{"class":196},"senZ8"," isFreshUpdate",[129,199,200],{"class":174},"(",[129,202,117],{"class":142},[129,204,205],{"class":174},",",[129,207,208],{"class":142}," now",[129,210,205],{"class":174},[129,212,213],{"class":142}," windowDays",[129,215,175],{"class":174},[129,217,218],{"class":142}," FRESH_WINDOW_DAYS",[129,220,221],{"class":174},")",[129,223,224],{"class":174}," {\n",[129,226,228,231,234,237,239,241,244],{"class":131,"line":227},4,[129,229,230],{"class":164},"  if",[129,232,233],{"class":174}," (",[129,235,236],{"class":145},"!",[129,238,117],{"class":142},[129,240,221],{"class":174},[129,242,243],{"class":164}," return",[129,245,246],{"class":164}," false\n",[129,248,250,253,256,258,261,264,267,269,271],{"class":131,"line":249},5,[129,251,252],{"class":145},"  const ",[129,254,255],{"class":142},"t",[129,257,175],{"class":174},[129,259,260],{"class":142}," Date",[129,262,263],{"class":174},".",[129,265,266],{"class":196},"parse",[129,268,200],{"class":174},[129,270,117],{"class":142},[129,272,273],{"class":174},")\n",[129,275,277,279,281,284,286,289,291,293,296,298],{"class":131,"line":276},6,[129,278,230],{"class":164},[129,280,233],{"class":174},[129,282,283],{"class":142},"Number",[129,285,263],{"class":174},[129,287,288],{"class":196},"isNaN",[129,290,200],{"class":174},[129,292,255],{"class":142},[129,294,295],{"class":174},"))",[129,297,243],{"class":164},[129,299,246],{"class":164},[129,301,303,305,308,310,312,314,317,320,323],{"class":131,"line":302},7,[129,304,252],{"class":145},[129,306,307],{"class":142},"diffMs",[129,309,175],{"class":174},[129,311,208],{"class":142},[129,313,263],{"class":174},[129,315,316],{"class":196},"getTime",[129,318,319],{"class":174},"()",[129,321,322],{"class":145}," - ",[129,324,325],{"class":142},"t\n",[129,327,329,332,335,338,341,344,346,349,351,354,357],{"class":131,"line":328},8,[129,330,331],{"class":164},"  return",[129,333,334],{"class":142}," diffMs",[129,336,337],{"class":174}," >=",[129,339,340],{"class":178}," 0",[129,342,343],{"class":145}," &&",[129,345,334],{"class":142},[129,347,348],{"class":174}," \u003C",[129,350,213],{"class":142},[129,352,353],{"class":145}," *",[129,355,356],{"class":142}," DAY_MS",[129,358,359],{"class":135},"  // 未来時刻（時計ズレ）は false\n",[129,361,363],{"class":131,"line":362},9,[129,364,365],{"class":174},"}\n",[10,367,368],{},"ポイントは「バッジを手で剥がす運用を最初から作らない」こと。月次データの更新は月初〜10日に集中するので、表示期間4日なら翌週明けには消える。判定はクライアント側の現在時刻で行い、SSGのHTMLには焼き込まない。ビルドし直さなくても時間経過だけでバッジが消灯する。",[10,370,371],{},"ブラウザで描画を確認させると、今朝更新した14銘柄にだけ NEW が点灯し、それ以外は通常表記のままだった。鮮度判定関数のカバレッジは100%。dataUpdatedAt なし・パース不能・未来時刻の異常系まで潰してある。",[14,373,375],{"id":374},"減少傾向はなかったよねを記憶ではなくデータで検証","「減少傾向はなかったよね？」を記憶ではなくデータで検証",[10,377,378],{},"バッジ実装のあと、ふと確認したくなった。「月次チャートで減少傾向にあるものは、あんまりなかったよね？」という自分の理解が合っているか。",[10,380,381],{},"記憶で答えを済ませず、レプリカDBの実データで全35銘柄の直近YoYと直近3ヶ月の傾向を機械的にスキャンさせた。結果、理解は概ね合っていたが例外が2つ見つかった。どちらもサーバー周辺チップのカテゴリで、2026年6月時点で前年割れしている。カードを目視で流していただけでは拾えていなかった例外だ。",[14,383,384],{"id":384},"学び",[22,386,387,390,393,396],{},[25,388,389],{},"「既存データのバグだ」と思っても、修正の前に周辺データまで機械的に照合させる。今回は自分の勘違いで、雑に直していたら正しい値を壊していた",[25,391,392],{},"表示の異常はスクリーンショットの見た目でなくDOMで判定する。意図的な装飾をバグと誤診しかけたら、その場でルールファイルに見分け方を書いておく",[25,394,395],{},"「更新を強調したい、でもずっと光るのは邪魔」という要望は、手動運用ではなくデータ鮮度による自動減衰で解ける。剥がし忘れという運用課題ごと消える",[25,397,398],{},"「〜はなかったはず」という自分の記憶は、DBがあるなら機械スキャンで裏を取る。35銘柄中2銘柄の例外は、目視では拾えていなかった",[400,401,402],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .stQ0i, html code.shiki .stQ0i{--shiki-default:#AB5959;--shiki-dark:#AB5959}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 .sHkkW, html code.shiki .sHkkW{--shiki-default:#1E754F;--shiki-dark:#1E754F}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sM54T, html code.shiki .sM54T{--shiki-default:#2F798A;--shiki-dark:#2F798A}html pre.shiki code .senZ8, html code.shiki .senZ8{--shiki-default:#59873A;--shiki-dark:#59873A}",{"title":125,"searchDepth":139,"depth":139,"links":404},[405,406,407,408,409,410],{"id":16,"depth":139,"text":17},{"id":55,"depth":139,"text":56},{"id":68,"depth":139,"text":69},{"id":101,"depth":139,"text":102},{"id":374,"depth":139,"text":375},{"id":384,"depth":139,"text":384},"dev",null,"台湾旅行で5日空いた統計チェーンを再開し、39銘柄の決算スナップショットと台湾ODM月次売上6月分を反映。更新銘柄を探す手間をなくすため、dataUpdatedAtによるNEWバッジの自動減衰ロジックを実装した記録。","md",{},"/memory-makers-new-badge","financial-data",false,"2026-07-13T00:00:00.000Z",{"title":5,"description":413},"2026-07/2026-07-13/memory-makers-new-badge",[423,424,425,426,427],"データ可視化","Nuxt","TypeScript","統計データ","Claude Code","OIljhMmubgaiIAq42bQw9qCv7i9kDrdKacqrB_cdcpE",[],"https://log.eurekapu.com/og/blog/memory-makers-new-badge.png?v=2026-07-13T00%3A00%3A00.000Z&title=%E6%9C%88%E6%AC%A1%E7%B5%B1%E8%A8%88%E3%82%AB%E3%83%BC%E3%83%89%E3%81%ABNEW%E3%83%90%E3%83%83%E3%82%B8%E3%82%92%E5%AE%9F%E8%A3%85%20-%20%E3%83%87%E3%83%BC%E3%82%BF%E9%AE%AE%E5%BA%A6%E3%81%A7%E8%87%AA%E5%8B%95%E7%82%B9%E7%81%AF%E3%83%BB%E8%87%AA%E5%8B%95%E6%B6%88%E7%81%AF%E3%81%95%E3%81%9B%E3%82%8B&author=Kei%20Komatsu&sig=b785d1359c52b03b",1784001815091]